import json
import math

import numpy as np
import pandas as pd
from loguru import logger
from scipy.optimize import differential_evolution

from scripts.constants.app_configuration import PARAMETER_INFO, PARAMETER_INPUT, POSTGRES_URI, MODEL, \
    KAIROS_DB_HOST, data_conf, TARGET_VALUE, OUTPUT_DATA
from scripts.core.data.data_import import DataPuller
from scripts.core.model_factory.model_loader import ModelLoader


class ParameterOptimization(object):
    def __init__(self, parameter_info=PARAMETER_INFO):
        self.original_bound = None
        self.parameter_info = parameter_info
        self.model = ModelLoader(model_info=MODEL).load_model()

    @staticmethod
    def calculate_current_bounds():
        data = "conf/profile.csv"
        df_profile = pd.read_csv(data)
        data_designated_profile = df_profile.to_dict(orient="records")
        bounds_data = dict()
        for each_record in data_designated_profile:
            bounds_data[each_record["column"]] = {
                "min": each_record["operating_min"],
                "max": each_record["operating_max"]
            }
        return df_profile, bounds_data

    def calculate_bounds(self, live_data, feed_input_columns):
        """
        :param live_data: dictionary of a single row of dataframe orient="records"
        :param feed_input_columns: list of columns of the type Feed Drum Inputs
        :return:
        """
        logger.info("Calculating Bounds for the data")
        try:
            temporary_bound_data = self.original_bound.copy()
            if self.parameter_info["non_controllable_params"] is not None:
                for each_parameter in self.parameter_info["non_controllable_params"]:
                    temporary_bound_data[each_parameter] = {"min": int(live_data[each_parameter]),
                                                            "max": int(live_data[each_parameter])}
            if feed_input_columns:
                for each_parameter in feed_input_columns:
                    temporary_bound_data[each_parameter] = {"min": int(live_data[each_parameter]) - 1,
                                                            "max": int(live_data[each_parameter]) + 1}

            formatted_bound_data = dict()
            for each_key in PARAMETER_INPUT:
                formatted_bound_data[each_key] = (
                    int(temporary_bound_data[each_key]["min"]), int(temporary_bound_data[each_key]["max"]))
            print("Formatted Bound Data", formatted_bound_data)
            return list(formatted_bound_data.values())
        except Exception as e:
            logger.error("Unable to calculate bounds for the data : {}".format(str(e)))

    def verify_operational_bounds(self, data_dict):
        temporary_bound_data = self.original_bound.copy()
        temporary_recommendation_data = data_dict.copy()
        operational_bound_flag = True
        for each_key in PARAMETER_INPUT:
            parameter = each_key
            parameter_lower = "{}_lower".format(parameter)
            parameter_upper = "{}_upper".format(parameter)
            parameter_original = "{}_original".format(parameter)
            if (data_dict[parameter_original] < temporary_bound_data[parameter]["min"]) or (
                    data_dict[parameter_original] > temporary_bound_data[parameter]["max"]):
                operational_bound_flag = False
            if data_dict[parameter_lower] < temporary_bound_data[parameter]["min"]:
                temporary_recommendation_data[parameter_lower] = temporary_bound_data[parameter]["min"]
            if data_dict[parameter_upper] > temporary_bound_data[parameter]["max"]:
                temporary_recommendation_data[parameter_upper] = temporary_bound_data[parameter]["max"]
        print("operational_bound_flag --> ", operational_bound_flag)
        print("temporary_recommendation_data --> ", temporary_recommendation_data)
        return operational_bound_flag

    def _objective_fun(self, x):
        temp_df = pd.DataFrame(columns=PARAMETER_INPUT, data=[x])
        return 0 - self.model.predict(temp_df)[0]

    def get_recommendation(self, live_processed_data_dict, bounds):
        rs = differential_evolution(
            self._objective_fun,
            bounds=bounds,
            seed=42,
            workers=1)
        return rs.x

    @staticmethod
    def find_beta_recommendation(at_value, au_value, am_value, an_value, ao_value):
        column_renamer_dict = {
            "Surge Tank Pyridine": "T",
            "Surge Tank Beta": "U",
            "AMM Rec Btm Pyr": "Y",
            "Amm Rec Btm Beta": "Z"
        }
        data = DataPuller(db_host=KAIROS_DB_HOST, data_config=data_conf, payload="beta_calculation_query").get_data()
        data = data.rename(columns=column_renamer_dict, inplace=False)
        data = data[["T", "U", "Y", "Z"]]
        data = data.to_dict(orient="records")[0]

        data["AT"] = at_value  # "Totalizers.FQ-5126",
        data["AU"] = au_value  # Totalizers.FQ-5263A
        data["AM"] = am_value  # FQ-5102 - FQ-6102
        data["AN"] = an_value  # FQ-5111 - FQ-6100
        data["AO"] = ao_value  # FQ-5161 - FQ-6104

        data["BG"] = data["AT"] * data["T"]
        data["BH"] = data["AT"] * data["U"]
        data["BM"] = data["AU"] * data["Y"]
        data["BN"] = data["AU"] * data["Z"]
        common_denominator = data["BG"] + data["BH"] + data["BM"] + data["BN"]
        data["RPT_Pyr"] = 100 * ((data["BM"] + data["BG"]) / common_denominator)
        data["RPT_Beta"] = 100 * ((data["BN"] + data["BH"]) / common_denominator)
        # (AM12+AN12+AO12)/((AT12*V12/100)+(AU12*AA12/100)
        norm_denominator = (data["AT"] * (data["T"] + data["U"])) + (data["AU"] * (data["Y"] + data["Z"]))
        rpt_value = (data["AM"] + data["AN"] + data["AO"]) / norm_denominator
        # R5 Standard Norm : y = 0.0006x2 - 0.0361x + 1.8569
        standard_norm = (0.0006 * data["RPT_Beta"] * data["RPT_Beta"]) - (0.0361 * data["RPT_Beta"]) + 1.8569
        data["RPT_Norm"] = 100 * rpt_value
        data["Standard_Norm"] = standard_norm
        return data["RPT_Beta"], data["RPT_Norm"], data["Standard_Norm"]

    def get_original_recommendation_values(self, data_dict, add_predict):
        logger.info("Recommending original trend")
        df_validate = pd.DataFrame(data_dict)
        original_data = df_validate.to_dict(orient="records")[0]
        df_validate = df_validate[PARAMETER_INPUT]

        live_data = df_validate.to_dict(orient="records")[0]
        live_processed_data = df_validate.iloc[0, :].to_numpy().reshape(1, -1)
        live_data_prediction = self.model.predict(live_processed_data)[0]
        live_data_prediction_15_min = live_data_prediction / 4
        logger.debug("Started Calculating the recommendation for the data!")
        df_recommendation = df_validate
        logger.debug("Recommendation Calculated and received!")
        recommendation = df_validate.iloc[0, :].to_numpy().reshape(1, -1)
        rec_array = np.array(recommendation).reshape(1, -1)
        prediction = self.model.predict(rec_array)[0]
        prediction_15_min = prediction / 4

        logger.info("Recommendations generated for last received live data.")

        recommended_data = df_recommendation.to_dict(orient="records")[0]
        # Recommending the upper limit and lower limit factor for the module
        # TI - 50 --> 48 & 52
        updated_recommendation = dict()
        for each_column in recommended_data:
            if each_column in self.original_bound:
                column_delta = self.original_bound[each_column]["max"] - self.original_bound[each_column]["min"]
                percentage_delta_lower = int(100 * (column_delta / self.original_bound[each_column]["min"]))
                percentage_delta_upper = int(100 * (column_delta / self.original_bound[each_column]["max"]))
                percentage_delta = percentage_delta_lower - percentage_delta_upper
                calculated_delta = (0.15 * percentage_delta) / 100

                if each_column in self.original_bound:
                    lower_recommendation = recommended_data[each_column] * (1 - calculated_delta)
                    upper_recommendation = recommended_data[each_column] * (1 + calculated_delta)
                    original_recommendation = recommended_data[each_column]
                else:
                    lower_recommendation = recommended_data[each_column] * 0.95
                    upper_recommendation = recommended_data[each_column] * 1.05
                    original_recommendation = recommended_data[each_column]
                updated_recommendation["{}_lower".format(each_column)] = math.floor(lower_recommendation)
                updated_recommendation["{}_upper".format(each_column)] = math.ceil(upper_recommendation)
                updated_recommendation["{}_original".format(each_column)] = round(original_recommendation, 2)

        predicted_result = dict()
        totalizer_max_value = 60000000000000
        predicted_totalizer_value = original_data[add_predict] + prediction_15_min
        if predicted_totalizer_value > totalizer_max_value:
            predicted_totalizer_value = predicted_totalizer_value - totalizer_max_value
        predicted_result[add_predict] = predicted_totalizer_value

        au_value_15_min = recommended_data["7302011030 Recovery Column-A.FI-5263A"] / 4
        am_value_15_min = recommended_data["Feed Drum.FIC-5102"] / 4
        an_value_15_min = recommended_data["Feed Drum.FIC-5111"] / 4
        ao_value_15_min = recommended_data["Feed Drum.FIC-5161"] / 4
        beta_prediction, rpt_norm_prediction, predicted_standard_norm = self.find_beta_recommendation(
            at_value=prediction_15_min,
            au_value=au_value_15_min,
            am_value=am_value_15_min,
            an_value=an_value_15_min,
            ao_value=ao_value_15_min)

        live_au_value_15_min = live_data["7302011030 Recovery Column-A.FI-5263A"] / 4
        live_am_value_15_min = live_data["Feed Drum.FIC-5102"] / 4
        live_an_value_15_min = live_data["Feed Drum.FIC-5111"] / 4
        live_ao_value_15_min = live_data["Feed Drum.FIC-5161"] / 4
        beta_live, rpt_norm_live, live_standard_norm = self.find_beta_recommendation(
            at_value=live_data_prediction_15_min,
            au_value=live_au_value_15_min,
            am_value=live_am_value_15_min,
            an_value=live_an_value_15_min,
            ao_value=live_ao_value_15_min)

        predicted_result["ai_yield_Beta Ratio"] = beta_prediction
        predicted_result["ai_yield_Beta Ratio Live"] = beta_live
        predicted_result["ai_yield_Predicted Norm"] = rpt_norm_prediction
        predicted_result["ai_yield_Live Norm"] = rpt_norm_live
        predicted_result["ai_yield_Norm_Delta"] = rpt_norm_prediction - rpt_norm_live
        predicted_result["ai_yield_Live_Standard_Norm"] = live_standard_norm
        predicted_result["ai_yield_Target_Standard_Norm"] = predicted_standard_norm
        predicted_result["ai_yield_current_totalizer_prediction"] = live_data_prediction_15_min
        predicted_result["ai_yield_recommendation_totalizer_prediction"] = prediction_15_min
        predicted_result["ai_yield_Beta_Ratio_Delta"] = beta_prediction - beta_live
        if (predicted_result["ai_yield_Beta_Ratio_Delta"] >= 0) & (predicted_result["ai_yield_Norm_Delta"] <= 0):
            recommendation_flag = True
        else:
            recommendation_flag = False
        logger.info("Recalculated recommendation flag is {}".format(str(recommendation_flag)))
        return recommendation_flag, updated_recommendation, predicted_result

    @staticmethod
    def update_recommendation_lookup(model_recommendation, engine, profile_data, recommendation_flag,
                                     recommendation_text):
        df_recommendation = profile_data
        recommendation_data = df_recommendation.to_dict(orient="records")

        updated_recommendation_data = list()
        for each_data in recommendation_data:
            temp_recommendation_data = each_data.copy()
            parameter = each_data["column"]
            parameter_lower = "{}_lower".format(parameter)
            parameter_upper = "{}_upper".format(parameter)
            parameter_original = "{}_original".format(parameter)
            if parameter_lower in model_recommendation and parameter_upper in model_recommendation:
                temp_recommendation_data["rec_lower"] = model_recommendation[parameter_lower]
                temp_recommendation_data["rec_upper"] = model_recommendation[parameter_upper]
                temp_recommendation_data["rec_original"] = model_recommendation[parameter_original]
                updated_recommendation_data.append(temp_recommendation_data)

        col = ["rec_lower", "rec_upper", "rec_original"]
        df_updated_recommendation = pd.DataFrame(updated_recommendation_data)
        df_updated_recommendation[col] = df_updated_recommendation[col].round(2)
        df_updated_recommendation['rec_status'] = recommendation_text
        df_updated_recommendation.to_csv("{}.csv".format(OUTPUT_DATA["tables"]["lookup"]), index=False)
        full_profile_data = "conf/profile.csv"
        df_full_profile = pd.read_csv(full_profile_data)
        # df_updated_recommendation.to_sql(OUTPUT_DATA["tables"]["lookup"], engine, if_exists="replace")
        # df_full_profile.to_sql(OUTPUT_DATA["tables"]["profile"], engine, if_exists="replace")

    def find_optimum(self, data_dict, add_predict, controllable=False):
        logger.info("Finding optimum data for the last received live data.")
        df_validate = pd.DataFrame(data_dict)
        df_validate["Bharuch-Unit 2.CTCMP.Stage IIA - Chlorination & Neutralization-I.Reactor R-7204.total_chlorine_flow"] = \
            df_validate["Bharuch-Unit 2.CTCMP.Stage IIA - Chlorination & Neutralization-I.Reactor R-7204.Flow 3"] + \
            df_validate["Bharuch-Unit 2.CTCMP.Stage IIA - Chlorination & Neutralization-I.Reactor R-7204.Flow 5"]
        data_1 = df_validate
        original_data = df_validate.to_dict(orient="records")[0]
        df_validate = df_validate[PARAMETER_INPUT]
        live_data = df_validate.to_dict(orient="records")[0]
        live_processed_data = df_validate.iloc[0, :].to_numpy().reshape(1, -1)
        live_data_prediction = self.model.predict(df_validate)[0]
        live_data_prediction_15_min = live_data_prediction / 4
        print("Live Data ---> ", data_dict[0])
        df_profile, live_bounds = self.calculate_current_bounds()
        self.original_bound = live_bounds
        feed_input_cols = self.parameter_info["feed_input"]
        bounds = self.calculate_bounds(live_data=live_data, feed_input_columns=feed_input_cols)
        logger.debug("Started Calculating the recommendation for the data!")
        recommendation = (list(map(float, self.get_recommendation(
            live_processed_data_dict=live_processed_data, bounds=bounds))))
        logger.debug("Recommendation Calculated and received!")
        df_recommendation = pd.DataFrame(columns=list(df_validate.columns), data=[recommendation])
        prediction = self.model.predict(df_recommendation)[0]
        logger.info("Recommendations generated for last received live data.")
        recommended_data = df_recommendation.to_dict(orient="records")[0]
        print("Predicted Value for Live -->", live_data_prediction)
        print("Predicted Value for Recommendation -->", prediction)
        # print("Value Experienced In Actual --> ", data_dict[0][TARGET_VALUE])
        print("Recommended Data -->", recommended_data)
        updated_recommendation = dict()
        for each_column in recommended_data:
            if each_column in self.original_bound:
                column_delta = self.original_bound[each_column]["max"] - self.original_bound[each_column]["min"]
                try:
                    percentage_delta_lower = int(100 * (column_delta / self.original_bound[each_column]["min"]))
                    percentage_delta_upper = int(100 * (column_delta / self.original_bound[each_column]["max"]))
                except:
                    percentage_delta_lower = column_delta * 2
                    percentage_delta_upper = column_delta
                percentage_delta = percentage_delta_lower - percentage_delta_upper
                calculated_delta = (0.15 * percentage_delta) / 100

                if each_column in self.original_bound:
                    lower_recommendation = max(self.original_bound[each_column]["min"],
                                               recommended_data[each_column] * (1 - calculated_delta))
                    upper_recommendation = min(self.original_bound[each_column]["max"],
                                               recommended_data[each_column] * (1 + calculated_delta))
                    original_recommendation = recommended_data[each_column]
                else:
                    lower_recommendation = recommended_data[each_column] * 0.95
                    upper_recommendation = recommended_data[each_column] * 1.05
                    original_recommendation = recommended_data[each_column]
                updated_recommendation["{}_lower".format(each_column)] = math.floor(lower_recommendation)
                updated_recommendation["{}_upper".format(each_column)] = math.ceil(upper_recommendation)
                updated_recommendation["{}_original".format(each_column)] = round(original_recommendation, 2)
        print("Updated Recommendation", json.dumps(updated_recommendation))
        predicted_result = dict()
        predicted_result["ai_yield_pure CTCMP_predicted"] = prediction
        predicted_result["ai_yield_pure CTCMP_live"] = live_data_prediction
        predicted_result["ai_yield_pure CTCMP_delta"] = prediction - live_data_prediction

        if predicted_result["ai_yield_pure CTCMP_delta"] >= 0:
            recommendation_flag = True
            final_recommendation_flag = True
            recommendation_text = "Operator to make changes based on recommendations!"
        else:
            recommendation_flag = False
            final_recommendation_flag = False
            recommendation_text = "Operator to make changes based on recommendations!"

        logger.info("Updating lookup with the latest recommendation")
        logger.debug("Attempting to write to the Postgres DB")
        print("Updated Reco:->", updated_recommendation)
        self.update_recommendation_lookup(model_recommendation=updated_recommendation, engine=POSTGRES_URI,
                                          recommendation_flag=recommendation_flag,
                                          recommendation_text=recommendation_text,
                                          profile_data=df_profile)
        logger.info("Recommendations shared for the last received live data.")

        logger.info("Recommendation flag is {}".format(str(recommendation_flag)))
        return final_recommendation_flag, updated_recommendation, predicted_result
