from backend.Property import Property
from typing import List
from itertools import groupby
from recommendations.FloorRecommendations import FloorRecommendations
from recommendations.WallRecommendations import WallRecommendations
from recommendations.RoofRecommendations import RoofRecommendations
from recommendations.VentilationRecommendations import VentilationRecommendations
from recommendations.FireplaceRecommendations import FireplaceRecommendations
from recommendations.LightingRecommendations import LightingRecommendations
from recommendations.SolarPvRecommendations import SolarPvRecommendations
from recommendations.WindowsRecommendations import WindowsRecommendations
from recommendations.HeatingRecommender import HeatingRecommender
from recommendations.HotwaterRecommendations import HotwaterRecommendations
from recommendations.SecondaryHeating import SecondaryHeating
from backend.ml_models.AnnualBillSavings import AnnualBillSavings


class Recommendations:
    """
    High level recommendations class, which sits above the measure specific recommendation classes
    """

    def __init__(
        self,
        property_instance: Property,
        materials: List,
        exclusions: List[str] = None,
    ):
        """
        :param property_instance: Instance of the Property class, for the home associated to property_id
        :param materials: List of materials to be used in the recommendations
        """

        self.property_instance = property_instance
        self.materials = materials
        self.exclusions = exclusions if exclusions else []

        self.floor_recommender = FloorRecommendations(property_instance=property_instance, materials=materials)
        self.wall_recomender = WallRecommendations(property_instance=property_instance, materials=materials)
        self.roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials)
        self.ventilation_recomender = VentilationRecommendations(
            property_instance=property_instance, materials=materials
        )
        self.fireplace_recommender = FireplaceRecommendations(property_instance=property_instance)
        self.lighting_recommender = LightingRecommendations(property_instance=property_instance, materials=materials)
        self.windows_recommender = WindowsRecommendations(property_instance=property_instance, materials=materials)
        self.solar_recommender = SolarPvRecommendations(property_instance=property_instance)
        self.heating_recommender = HeatingRecommender(property_instance=property_instance)
        self.hotwater_recommender = HotwaterRecommendations(property_instance=property_instance)
        self.secondary_heating_recommender = SecondaryHeating(property_instance=property_instance)

    def recommend(self):

        """
        This method runs the recommendations for the individual measures and then appends them to a list for output

        The recommendations are implemented in order of suggested phase, from fabric first to heating systems, to
        renewables.
        :return:
        """

        property_recommendations = []
        phase = 0

        # Building Fabric
        if "wall_insulation" not in self.exclusions:
            self.wall_recomender.recommend(phase=phase)
            if self.wall_recomender.recommendations:
                property_recommendations.append(self.wall_recomender.recommendations)
                phase += 1

        if "roof_insulation" not in self.exclusions:
            self.roof_recommender.recommend(phase=phase)
            if self.roof_recommender.recommendations:
                property_recommendations.append(self.roof_recommender.recommendations)
                phase += 1

        # Ventilation recommendations
        # We only produce a ventilation recommendation if the property is recommended to have wall or roof
        # insulation
        # We will not attribute a SAP impact to the ventilation recommendation, since we've seen that this
        # has no
        # real impact on the SAP score. Therefore, we don't need to include phasing for ventilation. If we
        # have any
        # wall or roof recommendations, we will ensure that ventilation is included in the simulation
        if "ventilation" not in self.exclusions:
            if self.wall_recomender.recommendations or self.roof_recommender.recommendations:
                self.ventilation_recomender.recommend()
                if self.ventilation_recomender.recommendation:
                    property_recommendations.append(self.ventilation_recomender.recommendation)

        if "floor_insulation" not in self.exclusions:
            self.floor_recommender.recommend(phase=phase)
            if self.floor_recommender.recommendations:
                property_recommendations.append(self.floor_recommender.recommendations)
                phase += 1

        if "windows" not in self.exclusions:
            self.windows_recommender.recommend(phase=phase)
            if self.windows_recommender.recommendation:
                property_recommendations.append(self.windows_recommender.recommendation)
                phase += 1

        if "fireplace" not in self.exclusions:
            self.fireplace_recommender.recommend(phase=phase)
            if self.fireplace_recommender.recommendation:
                property_recommendations.append(self.fireplace_recommender.recommendation)
                phase += 1

        # Heating and Electical systems
        if "heating" not in self.exclusions:

            cavity_or_loft_recommendations = [
                r for r in self.wall_recomender.recommendations + self.roof_recommender.recommendations
                if r["type"] in ["cavity_wall_insulation", "loft_insulation"]
            ]
            has_cavity_or_loft_recommendations = len(cavity_or_loft_recommendations) > 0

            self.heating_recommender.recommend(
                phase=phase, has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations
            )
            if (
                self.heating_recommender.heating_recommendations or
                self.heating_recommender.heating_control_recommendations
            ):

                # We split into first and second phase recommendations
                first_phase_recommendations = [
                    r for r in (
                        self.heating_recommender.heating_recommendations +
                        self.heating_recommender.heating_control_recommendations
                    )
                    if r["phase"] == phase
                ]
                second_phase_recommendations = [
                    r for r in (
                        self.heating_recommender.heating_recommendations +
                        self.heating_recommender.heating_control_recommendations
                    )
                    if r["phase"] == phase + 1
                ]

                if first_phase_recommendations:
                    property_recommendations.append(first_phase_recommendations)

                if second_phase_recommendations:
                    property_recommendations.append(second_phase_recommendations)

                # We check if we have distinct heating and heating controls recommendations
                # If so, we increment by 2 (one of the heating system, one for the heating controls)
                # otherwise we incremenet by 1
                max_used_phase = max(
                    [rec["phase"] for rec in
                     self.heating_recommender.heating_recommendations +
                     self.heating_recommender.heating_control_recommendations]
                )
                amount_to_increment = max_used_phase - phase + 1
                phase += amount_to_increment

        # Hot water
        if "hot_water" not in self.exclusions:
            self.hotwater_recommender.recommend(phase=phase)
            if self.hotwater_recommender.recommendations:
                property_recommendations.append(self.hotwater_recommender.recommendations)
                phase += 1

        if "lighting" not in self.exclusions:
            self.lighting_recommender.recommend(phase=phase)
            if self.lighting_recommender.recommendation:
                property_recommendations.append(self.lighting_recommender.recommendation)
                phase += 1

        if "secondary_heating" not in self.exclusions:
            self.secondary_heating_recommender.recommend(phase=phase)
            if self.secondary_heating_recommender.recommendation:
                property_recommendations.append(self.secondary_heating_recommender.recommendation)
                phase += 1

        # Renewables
        if "solar_pv" not in self.exclusions:
            self.solar_recommender.recommend(phase=phase)
            if self.solar_recommender.recommendation:
                property_recommendations.append(self.solar_recommender.recommendation)
                phase += 1

        # We insert temporary ids into the recommendations which is important for the optimiser later
        property_recommendations = self.insert_temp_recommendation_id(property_recommendations)

        # We also need to create the representative recommendations for each recommendation type
        property_representative_recommendations = self.create_representative_recommendations(
            property_recommendations, non_invasive_recommendations=self.property_instance.non_invasive_recommendations
        )

        return property_recommendations, property_representative_recommendations

    @staticmethod
    def create_representative_recommendations(property_recommendations, non_invasive_recommendations):
        """
        This method will create a representative recommendation for each recommendation type
        In order to create a representative recommendation, we choose the recommendation that has:
        1) Where a U-value is available, has the best U-value to cost ratio
        2) Where SAP points are available, has the best SAP points to cost ratio

        We don't include mechanical ventilation in the representative recommendations, since we don't attribute a
        SAP impact to this recommendation
        :return:
        """
        property_representative_recommendations = []

        for recommendations_by_type in property_recommendations:

            # If the property was initially surveyed as filled, but the cavity was only partially filled, we don't
            # want to include the cavity wall insulation recommendation in the defaults
            # if (recommendations_by_type[0].get("type") == "cavity_wall_insulation") and (
            #     "cavity_surveyed_as_filled_is_partial" in non_invasive_recommendations
            # ):
            #     continue

            if recommendations_by_type[0].get("type") == "mechanical_ventilation":
                continue

            has_u_value = recommendations_by_type[0].get("new_u_value") is not None
            has_sap_points = recommendations_by_type[0].get("sap_points") is not None

            # When check if these recommendations have two different types, such as solid wall insulation
            # If we have multiple types, we group by type and then select the best recommendation for each type

            recommendations_by_type = sorted(recommendations_by_type, key=lambda x: x["type"])
            representative_recommendations = []
            for _type, recommendations in groupby(recommendations_by_type, key=lambda x: x["type"]):
                recommendations = list(recommendations)
                # We also create an efficiency key, which is used to sort the recommendations
                if has_u_value:
                    # We sort by the cost per U-value improvement - the lower the better
                    for rec in recommendations:
                        rec["efficiency"] = rec["total"] / rec["starting_u_value"] - rec["new_u_value"]
                elif not has_u_value and has_sap_points:
                    # Sort the options by the cost per SAP point improvement - the lower the better
                    for rec in recommendations:
                        rec["efficiency"] = rec["total"] / rec["sap_points"]
                else:
                    # Sort the options by cost - the lower the better
                    for rec in recommendations:
                        rec["efficiency"] = rec["total"]

                recommendations.sort(
                    key=lambda x: x["efficiency"]
                )
                representative_recommendations.append(recommendations[0])
            property_representative_recommendations.extend(representative_recommendations)

        return property_representative_recommendations

    @staticmethod
    def insert_temp_recommendation_id(property_recommendations):
        """
        Creates a temporary recommendation id which is needed for
        filtering recommendations between default and no, after the optimiser has been
        run
        :param property_recommendations:  nested list of recommendations, grouped by data_types
        :return: Updated recommendations_to_upload, where where recommendation has a "recommendation_id"
                 integer inserted
        """
        idx = 0

        for recs in property_recommendations:
            for rec in recs:
                rec["recommendation_id"] = f"{str(idx)}_phase={str(rec['phase'])}"
                idx += 1

        return property_recommendations

    @classmethod
    def calculate_recommendation_impact(cls, property_instance, all_predictions, recommendations):

        """
        Given predictions from the model apis, with method will update the recommendations with the predicted
        impact of the recommendation on the property

        :param property_instance: Instance of the Property class, for the home associated to property_id
        :param all_predictions: dictionary of predictions from the model apis
        :param recommendations: dictionary of recommendations for the property
        :return:
        """

        property_sap_predictions = all_predictions["sap_change_predictions"][
            all_predictions["sap_change_predictions"]["property_id"] == str(property_instance.id)
            ].copy()
        property_heat_predictions = all_predictions["heat_demand_predictions"][
            all_predictions["heat_demand_predictions"]["property_id"] == str(property_instance.id)
            ].copy()
        property_carbon_predictions = all_predictions["carbon_change_predictions"][
            all_predictions["carbon_change_predictions"]["property_id"] == str(property_instance.id)
            ].copy()

        property_recommendations = recommendations[property_instance.id].copy()

        # We calculate the impact by phase
        sap_phase_impact = property_sap_predictions.groupby("phase")["predictions"].median().reset_index()
        heat_phase_impact = property_heat_predictions.groupby("phase")["predictions"].median().reset_index()
        carbon_phase_impact = property_carbon_predictions.groupby("phase")["predictions"].median().reset_index()

        # The heat demand change is the difference between the starting heat demand and the value at the final phase
        expected_heat_demand = property_instance.floor_area * (
            heat_phase_impact[heat_phase_impact["phase"] == max(heat_phase_impact["phase"])]["predictions"].values[0]
        )
        starting_heat_demand = (
            float(property_instance.data["energy-consumption-current"]) * property_instance.floor_area
        )

        # This is the unadjusted resulting heat demand
        predicted_heat_demand_change = starting_heat_demand - expected_heat_demand

        # TODO: This isn't quite right as this is based on EVERY possible measure, not just the ones that are
        #       actually implemented
        expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
            epc_energy_consumption=expected_heat_demand,
            current_epc_rating=property_instance.data["current-energy-rating"],
            total_floor_area=property_instance.floor_area
        )

        adjusted_heat_demand_change = (
            property_instance.current_adjusted_energy - expected_adjusted_energy
        )

        # TODO: We should determine if the home is gas & electricity or just electricity
        expected_energy_bill = AnnualBillSavings.calculate_annual_bill(expected_adjusted_energy)

        for recommendations_by_type in property_recommendations:
            for rec in recommendations_by_type:

                if rec["type"] == "mechanical_ventilation":
                    # We don't have a percieved sap impact of mechanical ventilation
                    continue

                new_heat_demand = property_heat_predictions[property_heat_predictions["recommendation_id"] == str(
                    rec["recommendation_id"]
                )]["predictions"].values[0]

                new_carbon = property_carbon_predictions[property_carbon_predictions["recommendation_id"] == str(
                    rec["recommendation_id"]
                )]["predictions"].values[0]

                new_sap = property_sap_predictions[property_sap_predictions["recommendation_id"] == str(
                    rec["recommendation_id"]
                )]["predictions"].values[0]

                if rec["phase"] == 0:
                    predicted_sap_points = new_sap - float(property_instance.data["current-energy-efficiency"])
                    predicted_co2_savings = float(property_instance.data["co2-emissions-current"]) - new_carbon
                    predicted_heat_demand = property_instance.floor_area * (
                        float(property_instance.data["energy-consumption-current"]) - new_heat_demand
                    )
                else:
                    previous_phase = rec["phase"] - 1
                    predicted_sap_points = (
                        new_sap - sap_phase_impact[sap_phase_impact["phase"] == previous_phase]["predictions"].values[0]
                    )
                    predicted_co2_savings = (
                        carbon_phase_impact[carbon_phase_impact["phase"] == previous_phase]["predictions"].values[0] -
                        new_carbon
                    )
                    predicted_heat_demand = property_instance.floor_area * (
                        heat_phase_impact[heat_phase_impact["phase"] == previous_phase]["predictions"].values[0] -
                        new_heat_demand
                    )

                if rec["type"] == "low_energy_lighting":
                    # For the moment, we cap the number of SAP points that can be achieved by ventilation at 2
                    rec["sap_points"] = min(predicted_sap_points, LightingRecommendations.SAP_LIMIT)
                    rec["co2_equivalent_savings"] = min(predicted_co2_savings, rec["co2_equivalent_savings"])
                    rec["heat_demand"] = min(predicted_heat_demand, rec["heat_demand"])
                else:
                    rec["sap_points"] = predicted_sap_points
                    rec["co2_equivalent_savings"] = predicted_co2_savings
                    rec["heat_demand"] = predicted_heat_demand

                # Round to 2 decimal places
                rec["sap_points"] = round(rec["sap_points"], 2)

                # We now calculate the adjusted heat demand for this recommendation, which is simply the percentage
                # of the total adjusted heat demand change. The percentage we use is this recommendation's percentage
                # of the total heat demand per square meter change

                rec["adjusted_heat_demand"] = adjusted_heat_demand_change * (
                    rec["heat_demand"] / predicted_heat_demand_change
                )
                # We make sure this is NOT below 0
                rec["adjusted_heat_demand"] = max(0, rec["adjusted_heat_demand"])

                # Depending on the property's tarriff, we calculate the amount of energy savings this measure will bring
                if property_instance.energy_source == "electricity":
                    rec["energy_cost_savings"] = AnnualBillSavings.estimate_electric(rec["adjusted_heat_demand"])
                elif property_instance.energy_source == "electricity_and_gas":
                    rec["energy_cost_savings"] = AnnualBillSavings.estimate(rec["adjusted_heat_demand"])
                else:
                    raise ValueError("Invalid value for energy source")

                if (rec["sap_points"] is None) and (rec["co2_equivalent_savings"] is None) or (
                    rec["heat_demand"] is None) or (rec["energy_cost_savings"] is None):
                    raise ValueError("sap points, co2 or heat demand is missing")

        return (
            property_recommendations,
            expected_adjusted_energy,
            expected_energy_bill
        )
