from recommendations.Costs import Costs, BOILER_UPGRADE_SCHEME_ASHP_VALUE
from recommendations.recommendation_utils import check_simulation_difference, override_costs
from backend.Property import Property
from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes
from etl.epc_clean.epc_attributes.HotWaterAttributes import HotWaterAttributes
from etl.epc_clean.epc_attributes.MainFuelAttributes import MainFuelAttributes
from recommendations.HeatingControlRecommender import HeatingControlRecommender


class HeatingRecommender:
    ELECTRIC_HEATING_DESCRIPTIONS = [
        "Room heaters, electric",
        "Electric storage heaters",
        "Electric storage heaters, radiators",
        "Portable electric heaters assumed for most rooms",
    ]

    high_heat_retention_contols_desc = "Controls for high heat retention storage heaters"

    def __init__(self, property_instance: Property):
        self.property = property_instance
        self.costs = Costs(self.property)

        self.heating_recommendations = []
        self.heating_control_recommendations = []

        self.has_electric_heating_description = (
            self.property.main_heating["clean_description"] in self.ELECTRIC_HEATING_DESCRIPTIONS
        )

    def is_high_heat_retention_valid(self):
        """
        Check conditions if high heat retention storage is valid
        :return:
        """

        # If the property has assumed electric heating, regardless of whether or not it has a mains connection, we
        # can consider hhr storage heaters
        electric_heating_assumed = (
            self.property.main_heating["clean_description"] in ["No system present, electric heaters assumed"]
        )

        return self.has_electric_heating_description or electric_heating_assumed

    def recommend(self, has_cavity_or_loft_recommendations, phase=0):
        """
        Produces heating recommendations
        :param has_cavity_or_loft_recommendations: boolean indicating if we have produced a cavity or loft insulation
        recommendation. If there are cavity or loft recommendations, the property would need to complete those measures
        before being able to get the boiler upgrade scheme benefits. The messaging in the front end would be to
        :param phase: indicates the phase of the retrofit programme
        """

        # TODO: We could have a system flush recommendation for an existing boiler, where there is no need to replace
        #       the boiler, but instead flushing the system will make it run more efficiently. There is a cost for this
        #       in the Costs class, stored as SYSTEM_FLUSH_COST

        self.heating_recommendations = []
        self.heating_control_recommendations = []
        # This first iteration of the recommender will provide very basic recommendation
        # We recommend heating controls based on the main heating system

        if self.is_high_heat_retention_valid():
            # Recommend high heat retention storage heaters
            # TODO: We need to allow for the possibility that the property aleady has storage heaters, but just
            #       needs the controls
            self.recommend_hhr_storage_heaters(phase=phase, system_change=True, heating_controls_only=False)

        # if the property has mains heating with boiler and radiators, we recommend optimal heating controls
        has_boiler = self.property.main_heating["clean_description"] in ["Boiler and radiators, mains gas"]

        # We also check that the property doesn't have a heating system, but it has access to the mains gas
        no_heating_has_mains = self.property.main_heating["clean_description"] in [
            'No system present, electric heaters assumed'
        ] and self.property.data["mains-gas-flag"]

        has_gas_heaters = (
            self.property.main_heating["clean_description"] in ["Room heaters, mains gas"] and
            self.property.data["mains-gas-flag"]
        )

        # We also check if the property has electric heating, but it has access to the mains gas
        electic_heating_has_mains = self.has_electric_heating_description and self.property.data["mains-gas-flag"]

        portable_heaters_has_mains = (
            self.property.main_heating["clean_description"] in ["Portable electric heaters assumed for most rooms"] and
            self.property.data["mains-gas-flag"]
        )

        if (
            has_boiler or
            no_heating_has_mains or
            electic_heating_has_mains or
            has_gas_heaters or
            portable_heaters_has_mains
        ):
            # This indicates that the home previously did not have a boiler in place and so would require
            # an overhaul to the system - right now, this is all reasons, apart from if there is an existing boiler
            system_change = not has_boiler
            exising_room_heaters = self.property.main_heating["clean_description"] in [
                "Room heaters, electric", "Room heaters, mains gas"
            ]

            self.recommend_boiler_upgrades(
                phase=phase, system_change=system_change, exising_room_heaters=exising_room_heaters
            )

        # We recommend air source heat pumps
        # Heat pumps are suitable for all property types:
        # https://energysavingtrust.org.uk/from-flats-to-terraced-houses-heat-pumps-are-suitable-for-all-property-types/
        # Just seems least probable for flats, so we'll allow houses and bungalows
        # In the future, we'll allow overrides, so that non-intrusive surveys can contradict these conditions
        # and either allow or prevent the recommendation of an air source heat pump

        if self.is_ashp_valid():
            self.recommend_air_source_heat_pump(
                phase=phase, has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations
            )

        return

    def is_ashp_valid(self):
        suitable_property_type = self.property.data["property-type"] in ["House", "Bungalow"]
        has_air_source_heat_pump = self.property.main_heating["has_air_source_heat_pump"]

        return suitable_property_type and not has_air_source_heat_pump

    def recommend_air_source_heat_pump(self, phase, has_cavity_or_loft_recommendations, _return=False):
        """
        This method will implement the recommendation for an air source heat pump
        This is ultimately an overhaul to the heating system and so is recommended as an alternative to other
        heating system recommendations
        :return:
        """

        controls_recommender = HeatingControlRecommender(self.property)
        controls_recommender.recommend(heating_description="Air source heat pump, radiators, electric")

        ashp_costs = self.costs.air_source_heat_pump()
        # We add the costs of the heating controls, onto each key in the costs dictionary
        if controls_recommender.recommendation:
            for key in ashp_costs:
                ashp_costs[key] += controls_recommender.recommendation[0][key]

        already_installed = "air_source_heat_pump" in self.property.already_installed
        if already_installed:
            ashp_costs = override_costs(ashp_costs)
            description = "The property already has an air source heat pump, no further action needed."
        else:
            if controls_recommender.recommendation:
                description = ("Install an air source heat pump, and upgrade heating controls to Smart Thermostats, "
                               "room sensors and smart radiator valves (time & temperature zone control).")
            else:
                description = "Install an air source heat pump."

            # If the property does not have existing cavity and loft insulation, we include a note that the cost
            # includes the boiler upgrade scheme and that the cavity and loft need to be treated, to ensure access
            # to the funding
            if has_cavity_or_loft_recommendations:
                description = description + (f" The cost includes the £"
                                             f"{BOILER_UPGRADE_SCHEME_ASHP_VALUE} boiler upgrade scheme grant. "
                                             f"You must ensure that the property has an insulated cavity and "
                                             f"270mm+ loft insulation to qualify for the grant")
            else:
                description = description + (f" The cost includes the £"
                                             f"{BOILER_UPGRADE_SCHEME_ASHP_VALUE} boiler upgrade scheme grant")

        simulation_config = {
            "mainheat_energy_eff_ending": "Good",
            "hot_water_energy_eff_ending": "Good"
        }
        # Installation of a boiler improves the hot water system so we need to reflect this in
        # the outcome of the recommendation
        heating_ending_config = MainHeatAttributes("Air source heat pump, radiators, electric").process()
        hotwater_ending_config = HotWaterAttributes("From main system").process()

        # If the property does not currently have electric main fuel, we'll simulate the change
        fuel_ending_config = {}
        if self.property.main_fuel["fuel_type"] != "electricity":
            fuel_ending_config = MainFuelAttributes("electricity (not community)").process()

        # Check the simulation differences
        heating_simulation_config = check_simulation_difference(
            new_config=heating_ending_config, old_config=self.property.main_heating
        )
        hotwater_simulation_config = check_simulation_difference(
            new_config=hotwater_ending_config, old_config=self.property.hotwater
        )
        fuel_simulation_config = check_simulation_difference(
            new_config=fuel_ending_config, old_config=self.property.main_fuel
        )

        simulation_config = {
            **simulation_config,
            **heating_simulation_config,
            **hotwater_simulation_config,
            **fuel_simulation_config,
        }

        if controls_recommender.recommendation:
            # We should have just the single recommendation for heat controls, which is time
            # and temperature zone controls
            if len(controls_recommender.recommendation) != 1:
                raise NotImplementedError("More than one heat controls recommendation for air source heat pump")
            simulation_config = {
                **simulation_config,
                **controls_recommender.recommendation[0]["simulation_config"]
            }

        ashp_recommendation = {
            "phase": phase,
            "parts": [
                # TODO
            ],
            "type": "heating",
            "description": description,
            "starting_u_value": None,
            "new_u_value": None,
            "sap_points": None,
            "already_installed": already_installed,
            "simulation_config": simulation_config,
            **ashp_costs
        }

        if _return:
            return [ashp_recommendation]
        self.heating_recommendations.append(ashp_recommendation)

    @staticmethod
    def check_simulation_difference(old_config, new_config):
        """
        Given two dictionaries, that describe the heating control configurations, this method will compare the two
        and pick out the differences. These differences will be things that have been added and things that have been
        removed. This will be used to determine how we should be updating the configuration in the simulation
        :return:
        """

        differences = {key + "_ending": new_config[key] for key in new_config if old_config[key] != new_config[key]}

        return differences

    def combine_heating_and_controls(
        self, controls_recommendations, heating_simulation_config, costs, description, phase, heating_controls_only,
        system_change
    ):
        """
        Given a recommendation for heating controls, and a recommendation for the heating system, we combine the two
        into a single recommendation
        :param controls_recommendations: The heating controls recommendations
        :param heating_simulation_config: The simulation configuration for the heating system
        :param costs: The costs of the heating system
        :param description: The description of the recommendation
        :param phase: The phase of the recommendation
        :param heating_controls_only: If True, we will also add a recommendation for heating controls only
        :param system_change: Indicates if we are recommending a different type of heating system, compared to the
        current system. If we have a system change and we have a heat control recommendation, we only recommend
        both heating and controls together
        :return:
        """

        # We produce recommendations with & without heating controls
        # We will also produce a recommendation for heating controls only
        heating_controls_switch = [True, False] if controls_recommendations else [False]
        if not heating_simulation_config:
            heating_controls_switch = []

        if system_change and len(controls_recommendations):
            heating_controls_switch = [True]

        output = []
        for controls_switch in heating_controls_switch:
            total_costs = costs.copy()
            recommendation_simulation_config = heating_simulation_config.copy()
            recommendation_description = description
            if controls_switch:
                # We add the costs of the heating controls, onto each key in the costs dictionary
                for key in total_costs:
                    total_costs[key] += controls_recommendations[0][key]

                recommendation_simulation_config = {
                    **recommendation_simulation_config,
                    **controls_recommendations[0]["simulation_config"]
                }
                controls_description = controls_recommendations[0]['description']
                # Make the first letter of the description lowercase
                controls_description = (
                    controls_description[0].lower() + controls_description[1:]
                )

                recommendation_description = f"{description} and {controls_description}"

            already_installed = "heating_controls" in self.property.already_installed
            if already_installed:
                total_costs = override_costs(total_costs)
                recommendation_description = "Heating system has already been upgraded, no further action needed."

            recommendation = {
                "phase": phase,
                "parts": [
                    # TODO
                ],
                "type": "heating",
                "description": recommendation_description,
                "starting_u_value": None,
                "new_u_value": None,
                "sap_points": None,
                "already_installed": already_installed,
                **total_costs,
                "simulation_config": recommendation_simulation_config
            }

            output.append(recommendation)

        if heating_controls_only and len(controls_recommendations):
            # Also add on a recommendation for heating controls only
            heating_control_recommendation = controls_recommendations[0].copy()
            # Capitalize the first letter of the description
            heating_control_recommendation["description"] = (
                heating_control_recommendation["description"][0].upper() +
                heating_control_recommendation["description"][1:]
            )

            output.append(
                {
                    "phase": phase,
                    "parts": [
                        # TODO
                    ],
                    "type": "heating",
                    "starting_u_value": None,
                    "new_u_value": None,
                    "sap_points": None,
                    **heating_control_recommendation
                }
            )

        return output

    def is_hhr_already_installed(self):
        """
        Check if the property already has high heat retention storage heaters
        :return:
        """

        already_has_hhr = "Electric storage heaters" in self.property.main_heating["clean_description"]

        # Some electric storage heaters will show that the controls are "Manual charge controls" which are indicative
        # of the old model of electric storage heaters, originating from 1961.
        # Newer HHR storage heaters will charge up over night but will retain the heat durin the day for when warmth
        # is actually needed, unlike traditional storage heaters that charge up at night and release heat during the day
        # which isn't always ideal for the occupants.
        already_has_hhr_contols = (
            self.property.main_heating_controls[
                "clean_description"
            ].lower() == self.high_heat_retention_contols_desc.lower()
        )

        return already_has_hhr and already_has_hhr_contols

    def recommend_hhr_storage_heaters(self, phase, system_change, heating_controls_only, _return=False):
        """
        We will recommend upgrading to a high heat retention storage system, if the current system is not already
        high heat retention storage

        :param phase: The phase of the recommendation
        :param system_change: Indicates if we are recommending a different type of heating system, compared to the
        current system
        :param heating_controls_only: Indicates if we should include a recommendation for just heating controls
        :param _return: Indicates if we should return the recommendations, rather than appending them to the
                        recommendations list
        :return:
        """

        controls_recommender = HeatingControlRecommender(self.property)
        # The heating controls we're recommending for are based on the recommended heating system

        # We only recommend Celect-type controls if the current heating system is not Celect-type controls
        if self.property.main_heating_controls["clean_description"] != self.high_heat_retention_contols_desc:
            controls_recommender.recommend(heating_description="Electric storage heaters, radiators")

        has_hhr = self.is_hhr_already_installed()
        # Conditions for not recommending electric storage heaters
        if has_hhr:
            # No recommendation needed
            return

        # Set up artefacts, suitable for the simulation and regardless of controls
        heating_ending_config = MainHeatAttributes("Electric storage heaters, radiators").process()
        heating_simulation_config = check_simulation_difference(
            new_config=heating_ending_config, old_config=self.property.main_heating
        )
        # This upgrade will only take the heating system to average energy efficiency
        heating_simulation_config["mainheat_energy_eff_ending"] = "Average"

        # If the property is off-gas and has no heating system in place, the number of heated rooms will actually
        # be 0, so we use the number of rooms as the figure
        number_heated_rooms = (
            self.property.data["number-heated-rooms"] if self.property.data["number-heated-rooms"] > 0
            else (
                self.property.number_of_rooms - 1 if self.property.number_of_rooms > 1 else
                self.property.number_of_rooms
            )
        )
        # Upgrade to electric storage heaters
        costs = self.costs.high_heat_electric_storage_heaters(
            number_heated_rooms=number_heated_rooms
        )
        description = "Install high heat retention electric storage heaters"

        recommendations = self.combine_heating_and_controls(
            controls_recommendations=controls_recommender.recommendation,
            heating_simulation_config=heating_simulation_config,
            costs=costs,
            description=description,
            phase=phase,
            heating_controls_only=heating_controls_only,
            system_change=system_change
        )
        if _return:
            return recommendations

        self.heating_recommendations.extend(recommendations)

    @staticmethod
    def estimate_boiler_size(property_type, built_form, floor_area, floor_height, num_heated_rooms):
        # Step 1: Base size estimation based on property type (as a starting point)
        base_size = {
            'Flat': 25,
            'House': 30,
            'Maisonette': 28,
            'Bungalow': 27
        }

        # Step 2: Calculate the volume of the property
        volume = floor_area * floor_height

        # Step 3: Adjust base size for built form (to account for heat retention)
        form_adjustment = {
            'Mid-Terrace': 0,
            'End-Terrace': 2,
            'Semi-Detached': 4,
            'Detached': 6
        }

        # Step 4: Further adjust for the total volume and number of heated rooms
        volume_adjustment = (volume / 100)  # Simplified adjustment factor for volume
        rooms_adjustment = (num_heated_rooms - 5) * 0.5  # Assuming base case of 5 rooms

        # Calculate the estimated boiler size
        estimated_size = base_size[property_type] + form_adjustment[built_form] + volume_adjustment + rooms_adjustment

        # Step 5: Align with available boiler sizes and ensure it does not exceed 35kW, as it's rare to need more
        available_sizes = [30, 35, 40, 45, 50]
        estimated_size = min(max(estimated_size, 30), 40)  # Ensure within 30kW to 35kW range

        # Find the closest available size (in this case, either rounding up or down to align with 30 or 35)
        closest_size = min(available_sizes, key=lambda x: abs(x - estimated_size))

        return closest_size

    def recommend_boiler_upgrades(self, phase, system_change, exising_room_heaters):
        """
        This boiler recommendation will only recommend a like-for-like upgrade, since changing the system
        is generally more expensive
        :param phase:
        :param system_change: Indicates if the property would be undergoing a heating system change. This could be true
                              if the home didn't have a heating system in place, or if the home had electric heating
                              previously
        :param exising_room_heaters: Indicates if the property had room heaters previously - if so, a boiler
                                     recommendation will need to be accompanied by removal of the room heaters
        :return:
        """

        recommendation_phase = phase

        # We now recommend boiler upgrades, if applicable
        simulation_config = {}
        boiler_costs = {}
        boiler_recommendation = {}

        has_inefficient_space_heating = self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor", "Average"]

        has_inefficient_mains_water = (
            self.property.hotwater["clean_description"] in ["From main system"] and
            self.property.data["hot-water-energy-eff"] in ["Very Poor", "Poor", "Average"]
        )

        if has_inefficient_space_heating or has_inefficient_mains_water:
            boiler_size = self.estimate_boiler_size(
                property_type=self.property.data["property-type"],
                built_form=self.property.data["built-form"],
                floor_area=self.property.floor_area,
                floor_height=self.property.floor_height,
                num_heated_rooms=self.property.data["number-heated-rooms"],
            )

            description = "Upgrade to a new condensing boiler"

            simulation_config = {
                "mainheat_energy_eff_ending": "Good",
                "hot_water_energy_eff_ending": "Good"
            }
            if system_change:
                # Installation of a boiler improves the hot water system so we need to reflect this in
                # the outcome of the recommendation
                heating_ending_config = MainHeatAttributes("Boiler and radiators, mains gas").process()
                hotwater_ending_config = HotWaterAttributes("From main system").process()
                fuel_ending_config = MainFuelAttributes("mains gas (not community)").process()

                heating_simulation_config = check_simulation_difference(
                    new_config=heating_ending_config, old_config=self.property.main_heating
                )
                hotwater_simulation_config = check_simulation_difference(
                    new_config=hotwater_ending_config, old_config=self.property.hotwater
                )
                fuel_simulation_config = check_simulation_difference(
                    new_config=fuel_ending_config, old_config=self.property.main_fuel
                )

                simulation_config = {
                    **simulation_config,
                    **heating_simulation_config,
                    **hotwater_simulation_config,
                    **fuel_simulation_config,
                }

            boiler_costs = self.costs.boiler(
                size=f"{boiler_size}kw",
                exising_room_heaters=exising_room_heaters,
                system_change=system_change,
                n_heated_rooms=self.property.data["number-heated-rooms"],
                n_rooms=self.property.number_of_rooms
            )

            already_installed = "heating" in self.property.already_installed
            if already_installed:
                boiler_costs = override_costs(boiler_costs)
                description = "Heating system has already been upgraded, no further action needed."

            boiler_recommendation = {
                "phase": recommendation_phase,
                "parts": [
                    # TODO
                ],
                "type": "heating",
                "description": description,
                "starting_u_value": None,
                "new_u_value": None,
                "sap_points": None,
                "already_installed": already_installed,
                "simulation_config": simulation_config,
                **boiler_costs
            }

        # We recommend the heating controls
        # If the property did not previously have a boiler, we combine
        controls_recommender = HeatingControlRecommender(self.property)
        controls_recommender.recommend(heating_description="Boiler and radiators, mains gas")
        # We may have 2 recommendations from the heating controls

        if not controls_recommender.recommendation and not boiler_recommendation:
            return

        if not system_change and len(boiler_recommendation):
            # If there is not a system change, we add the boiler recommendation at point.
            self.heating_recommendations.extend([boiler_recommendation])

        if system_change:
            # We combine the heating and controls recommendations, in the case of a system change
            combined_recommendations = []
            for controls_recommendation in controls_recommender.recommendation:
                combined_recommendation = self.combine_heating_and_controls(
                    controls_recommendations=[controls_recommendation],
                    heating_simulation_config=simulation_config,
                    costs=boiler_costs,
                    description=boiler_recommendation["description"],
                    phase=recommendation_phase,
                    heating_controls_only=False,
                    system_change=True
                )
                combined_recommendations.extend(combined_recommendation)

            # Overwrite the existing boiler recommendation
            self.heating_recommendations.extend(combined_recommendations)
        else:
            # We increment the recommendation phase, since the heating controls are separate from the boiler upgrade
            # but we'll only upgrade if we have a heating recommendation
            has_heating_recommendation = any(
                rec["type"] == "heating" for rec in self.heating_recommendations
            )
            if has_heating_recommendation:
                recommendation_phase += 1
            # The heating controls recommendation is distrinct from the boiler upgrade recommendation
            # We insert phase into the recommendations for heating controls
            for recommendation in controls_recommender.recommendation:
                recommendation["phase"] = recommendation_phase

            self.heating_control_recommendations.extend(controls_recommender.recommendation)

        return
