import re
import backend.app.assumptions as assumptions
from recommendations.recommendation_utils import (
    check_simulation_difference, override_costs, combine_recommendation_configs, check_use_survey
)
from backend.Property import Property
from backend.app.plan.schemas import MEASURE_MAP
from recommendations.Costs import Costs
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
from utils.logger import setup_logger

logger = setup_logger()


class HeatingRecommender:
    high_heat_retention_contols_desc = "Controls for high heat retention storage heaters"

    DUAL_HEATING_DESCRIPTIONS = {
        "Boiler and radiators, mains gas, electric storage heaters": {
            "hhr": {
                "mainheating_description": "Boiler and radiators, mains gas, Electric storage heaters",
                "recommendation_description": "Install high heat retention electric storage heaters alongside the "
                                              "boiler. The current electric heaters may be retrofit with high heat "
                                              "retention storage controls"
                                              " however this is dependent on the existing system and may not be "
                                              "possible.",
                "controls_prefix": "current_controls"
            },
            "boiler": {
                "mainheating_description": "Boiler and radiators, mains gas, electric storage heaters",
                "recommendation_description": "Upgrade the existing boiler to a new, more efficient condensing "
                                              "boiler. ",
                "controls_suffix": "Manual charge controls"
            },
            # These are the heating types we need to produce a dual heating recommendation
            "dual": {
                "recommendation_description": "Upgrade both the existing boiler to a new condensing boiler and"
                                              " upgrade storage heaters to high heat retention storage heaters.",
                "types": [
                    # type 1
                    "boiler_upgrade",
                    # type 2
                    "high_heat_retention_storage_heaters",
                ]
            }
        },
        "Boiler and radiators, mains gas, electric underfloor heating": {
            "boiler": {
                "mainheating_description": "Boiler and radiators, mains gas, electric underfloor heating",
                "recommendation_description": "Upgrade the existing boiler to a new, more efficient condensing "
                                              "boiler. ",
                "controls_suffix": "Manual charge controls"
            },
            # These are the heating types we need to produce a dual heating recommendation
            "dual": {
                "recommendation_description": "Upgrade the existing boiler to a new condensing boiler",
                "types": [
                    # type 1
                    "boiler_upgrade",
                ]
            }
        },
        "Portable electric heaters assumed for most rooms, room heaters, electric": {
            "hhr": {
                "mainheating_description": "Electric storage heaters, radiators",
                "recommendation_description": "Install high heat retention electric storage heaters.",
                "controls_prefix": ""
            },
            "boiler": {
                "mainheating_description": "Boiler and radiators, mains gas",
                "recommendation_description": "Upgrade to a new condensing boiler.",
                "controls_suffix": ""
            },
            # These are the heating types we need to produce a dual heating recommendation
            "dual": None
        },
        'Electric underfloor heating, electric storage heaters': {
            # For this, we would recommend a heat pump
            "dual": None
        },
        "Room heaters, electric, boiler and radiators, mains gas": {
            "hhr": {
                "mainheating_description": "Electric storage heaters, radiators",
                "recommendation_description": "Install high heat retention electric storage heaters.",
                "controls_prefix": ""
            },
            "boiler": {
                "mainheating_description": "Boiler and radiators, mains gas",
                "recommendation_description": "Upgrade to a new condensing boiler.",
                "controls_suffix": ""
            },
            "dual": None
        },
        "Room heaters, electric, electric storage heaters": {
            "hhr": {
                "mainheating_description": "Electric storage heaters, radiators",
                "recommendation_description": "Install high heat retention electric storage heaters.",
                "controls_prefix": ""
            },
            "dual": None
        },
        'Electric storage heaters, room heaters, electric': {
            "hhr": {
                "mainheating_description": "Electric storage heaters, radiators",
                "recommendation_description": "Install high heat retention electric storage heaters.",
                "controls_prefix": ""
            },
            "dual": None
        }
    }

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

        self.heating_recommendations = []

        self.has_electric_heating_description = (
            self.property.main_heating["has_electric"] or self.property.main_heating["has_electricaire"]
        )
        self.has_ashp = self.property.main_heating["has_air_source_heat_pump"]
        self.has_gshp = self.property.main_heating["has_ground_source_heat_pump"]
        self.has_room_heaters = (
            self.property.main_heating["has_room_heaters"] or
            self.property.main_heating["has_portable_electric_heaters"]
        )
        self.has_boiler = self.property.main_heating["has_boiler"]

        self.dual_heating = self.identify_dual_heating()

        # Split out the different materials
        self.hhrsh_products = [
            product for product in materials if product["type"] == "high_heat_retention_storage_heaters"
        ]

    def identify_dual_heating(self):
        # All heat systems are in here so we identify whether two of these are true
        # MainHeatAttributes.HEAT_SYSTEMS

        if "sap05" in self.property.main_heating["clean_description"].lower():
            return False

        n_trues = 0
        for heat_system in MainHeatAttributes.HEAT_SYSTEMS:
            if self.property.main_heating[f"has_{heat_system.replace(' ', '_')}"]:
                n_trues += 1

        if n_trues > 2 or n_trues == 0:
            raise NotImplementedError("Implement me, zero or more than two heating systemss")
        if n_trues == 1:
            return False

        return True

    def is_high_heat_retention_valid(self, ashp_only_heating_recommendation, measures):
        """
        Check conditions if high heat retention storage is valid
        If there's already an ASHP in place, we don't recommend HHR
        :return:
        """

        # We can also recommend hhr if the property doesn't have a mains has connection
        no_mains = not self.property.epc_record.mains_gas_flag

        # If the property already has room heaters then we recommend HHR as an option since the home already has
        # a variation of room heaters

        hhr_suitable = no_mains or self.has_electric_heating_description or self.has_room_heaters

        # If the property has community heating heaters in place, we don't recommend HHRSH
        has_community_heating = self.property.main_fuel["is_community"]

        # If the property currently has electric underfloor heating, we allow this if there is elecric immersion
        # hot water heating
        underfloor_not_an_issue = True
        if self.property.main_heating["has_electric_underfloor_heating"]:
            if self.property.hotwater["heater_type"] != "electric immersion":
                underfloor_not_an_issue = False

        hhr_suitable = hhr_suitable and not has_community_heating and underfloor_not_an_issue

        # If the property has a ground source heat pump, or air source heat pump, we don't recommend HHRSH

        return (
            hhr_suitable and (not ashp_only_heating_recommendation) and not self.has_ashp and not self.has_gshp and
            ("high_heat_retention_storage_heaters" in measures)
        )

    def is_boiler_upgrade_suitable(self, measures, ashp_only_heating_recommendation):
        """
        These are the conditions we apply to recommend a boiler installation
        :return:
        """

        # 1) if the property has mains heating with boiler and radiators, we recommend optimal heating controls
        # If it's NOT a gas boiler, we'll potentially recommend a boiler
        has_gas_boiler = self.has_boiler and self.property.main_heating["has_mains_gas"]

        # 2) If 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.epc_record.mains_gas_flag

        # The property is using portable heaters and has access to gas mains
        has_room_heaters = self.has_room_heaters and self.property.epc_record.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.epc_record.mains_gas_flag

        portable_heaters_has_mains = (
            self.property.main_heating["has_portable_electric_heaters"] and self.property.epc_record.mains_gas_flag
        )

        # The next condition is if the home has a non-gas boiler, such as an oil boiler, with a mains gas connection
        non_gas_boiler = (
            self.property.main_heating["has_boiler"] and
            not self.property.main_heating["has_mains_gas"] and
            self.property.epc_record.mains_gas_flag
        )
        # Additionally, if the property has a gas connection, is using gas heating but doesn't have a boiler,
        # we recommend a boiler
        non_boiler_gas_heating = (
            self.property.epc_record.mains_gas_flag and
            self.property.main_heating["has_mains_gas"] and
            not self.property.main_heating["has_boiler"]
        )

        is_valid = (
            (
                has_gas_boiler or
                no_heating_has_mains or
                electic_heating_has_mains or
                has_room_heaters or
                portable_heaters_has_mains or
                non_gas_boiler or
                non_boiler_gas_heating
            ) and
            (not ashp_only_heating_recommendation) and
            ("boiler_upgrade" in measures) and
            (not self.has_ashp) and
            (not self.property.main_heating["has_warm_air"])
        )

        return is_valid, has_gas_boiler

    def recommend_dual_heating(self):

        if self.property.main_heating["clean_description"] not in self.DUAL_HEATING_DESCRIPTIONS:
            return

        # if we have set dual to None, we do not produce a dual heating recommendation
        if self.DUAL_HEATING_DESCRIPTIONS[
            self.property.main_heating["clean_description"]
        ]["dual"] is None:
            return

        dual_heating_description = self.DUAL_HEATING_DESCRIPTIONS[
            self.property.main_heating["clean_description"]
        ]["dual"]["types"]

        heating_measures = [
            m for m in self.heating_recommendations if
            m["measure_type"] not in ["time_temperature_zone_control", "roomstat_programmer_trvs"]
        ]

        recommendation_system_types = list(
            set([x["system_type"] for x in heating_measures])
        )

        # We check if we have the required type
        if not any([x in recommendation_system_types for x in dual_heating_description]):
            return

        type_1_recommendations = [
            x for x in heating_measures if x["system_type"] == dual_heating_description[0]
        ]
        type_2_recommendations = [
            x for x in heating_measures if x["system_type"] == dual_heating_description[1]
        ]
        # we combine the two recommendations
        combined_recommendations = []
        for rec in type_1_recommendations:
            for rec2 in type_2_recommendations:
                combined_rec = rec.copy()
                # Update the description
                combined_rec["description"] = self.DUAL_HEATING_DESCRIPTIONS[
                    self.property.main_heating["clean_description"]
                ]["dual"]["recommendation_description"]

                # Combine simulation_config
                # Make sure we end up with the best efficiecy values
                combined_rec["simulation_config"] = combine_recommendation_configs(
                    rec["simulation_config"], rec2["simulation_config"]
                )
                # Combine description_simulation
                combined_rec["description_simulation"] = combine_recommendation_configs(
                    rec["description_simulation"], rec2["description_simulation"]
                )

                # Combine costs
                for k in ["total", "subtotal", "vat", "labour_hours", "labour_days"]:
                    combined_rec[k] = rec[k] + rec2[k]

                combined_rec["measure_type"] = "+".join([rec["measure_type"], rec2["measure_type"]])

                combined_recommendations.append(combined_rec)

        self.heating_recommendations.extend(combined_recommendations)

    def recommend(self, has_cavity_or_loft_recommendations, phase=0, measures=None):
        """
        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
        :param measures: A list of measures for the recommendations
        """

        if "sap05" in self.property.main_heating["clean_description"].lower():
            return

        measures = MEASURE_MAP["heating"] if measures is None else measures

        # if we have a non-invasive ashp recommendation, we get the configuration directly from the property instance
        non_invasive_ashp_recommendation = next(
            (r for r in self.property.non_invasive_recommendations if r["type"] == "air_source_heat_pump"),
            {"survey": False}
        )
        # We allow for the non-invasive recommendation to be that ASHP is not suitable

        # This option will prevent other heating recommendations from being specified, other than an ASHP
        ashp_only_heating_recommendation = non_invasive_ashp_recommendation.get(
            "ashp_only_heating_recommendation", False
        )
        self.heating_recommendations = []
        # This first iteration of the recommender will provide very basic recommendation
        # We recommend heating controls based on the main heating system

        hhr_valid = self.is_high_heat_retention_valid(ashp_only_heating_recommendation, measures)

        if hhr_valid:
            # Recommend high heat retention storage heaters
            self.recommend_hhr_storage_heaters(phase=phase, system_change=True, heating_controls_only=False)

        gas_boiler_suitable, has_gas_boiler = self.is_boiler_upgrade_suitable(
            measures=measures, ashp_only_heating_recommendation=ashp_only_heating_recommendation
        )

        if gas_boiler_suitable:
            # 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_gas_boiler
            exising_room_heaters = self.property.main_heating["has_room_heaters"]

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

        # If we have dual heating and we allow for a combined recommendation, to upgrade both systems
        if self.dual_heating:
            self.recommend_dual_heating()

        # 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.property.is_ashp_valid(measures=measures) and
            len(non_invasive_ashp_recommendation) and
            not self.has_ashp and not self.has_gshp
        ):
            self.recommend_air_source_heat_pump(
                phase=phase,
                has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations,
            )

        return

    def recommend_electric_boiler_upgrade(self, phase):

        # Small initial scope, just handles the case of properties that have electric boilers where the efficiency
        # is poor or very poor
        # We recommend upgrading to a new electric boiler

        recommendation_phase = phase

        if self.property.epc_record.mainheat_energy_eff not in ["Poor", "Very Poor"]:
            return

        hotwater_from_mains = self.property.hotwater["clean_description"] in ["From main system"]
        hotwater_from_cylinder = self.property.hotwater["clean_description"] in [
            "From main system, no cylinder thermostat"
        ]
        # if the hotwater is from the mains, we probably have a combi boiler so we recommend a new electric boiler

        if hotwater_from_mains:
            description = f"Upgrade to a higher efficiency electric boiler"

            simulation_config = {
                "mainheat_energy_eff_ending": "Average",
                "hot_water_energy_eff_ending": "Average"
            }

            boiler_costs = self.costs.boiler(
                size=None,
                exising_room_heaters=False,
                system_change=False,
                n_heated_rooms=self.property.epc_record.number_heated_rooms,
                n_rooms=self.property.number_of_rooms,
                is_electric=True
            )

            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": [],
                "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
            }

            controls_recommender = HeatingControlRecommender(self.property)
            controls_recommender.recommend(heating_description="Boiler and radiators, electric", phase=phase)

            self.heating_recommendations.extend([boiler_recommendation] + controls_recommender.recommendation)
            return

        if hotwater_from_cylinder:
            # We recommend a change from a system boiler, with a cylinder to a combi boiler
            description = ("Replace the existing boiler and cylinder without a thermostat with a new electric combi "
                           "boiler")

    def size_heat_pump(self):
        """
        Given the methodology by installers (SCIS) this function will perform a basic heat loss calculation and
        produce a recommendation for the size of the heat pump
        :return:
        """

        floor_area = self.property.floor_area

        # We use the default heat loss W/m2 values are specified by the insaller, depending on the property type

        def remap_to_heat_loss(construction_age_band):
            if "before 1900" in construction_age_band:
                return "Pre 1900 (solid stone)"
            elif "1900-1929" in construction_age_band:
                return "Early 1900s (solid brick)"
            elif re.search(r'1930|1949|1950|1966|1967|1975', construction_age_band):
                return "1950-1980 (cavity void)"
            elif re.search(r'1976|1982|1983|1990', construction_age_band):
                return "Post 1980 (cavity wall construction)"
            elif re.search(r'1991|1995|1996|2002|2003|2011', construction_age_band):
                return "2000-2018"
            elif "2012 onwards" in construction_age_band:
                return "New build (2018+)"
            else:
                return None

        def select_heatpump_size(heat_loss_calculation):
            """
            This function calculates the size of the heat pump based on the heat loss calculation, mapping
            the heat loss calculation to the size of the heat pump in KW
            :param heat_loss_calculation: This is calcualted as the floor area multipled by the heat loss constant,
            divided by 1000
            """
            if heat_loss_calculation < 5:
                return 5
            elif 5 <= heat_loss_calculation < 6:
                return 6
            elif 6 <= heat_loss_calculation < 8.5:
                return 8.5
            elif 8.5 <= heat_loss_calculation < 11.2:
                return 11.2
            elif 11.2 <= heat_loss_calculation < 14:
                return 14
            elif 14 <= heat_loss_calculation < 17:
                return 17
            elif 17 <= heat_loss_calculation < 20:
                return 20
            else:
                return None

        heat_loss_constants = {
            "New build (2018+)": 35,
            "2000-2018": 50,
            "Post 1980 (cavity wall construction)": 60,
            "1950-1980 (cavity void)": 70,
            "Early 1900s (solid brick)": 80,
            "Pre 1900 (solid stone)": 90
        }

        heat_loss_group = remap_to_heat_loss(self.property.construction_age_band)
        heat_loss_constant = heat_loss_constants[heat_loss_group]

        heat_loss_calculation = floor_area * heat_loss_constant / 1000

        heat_pump_size = select_heatpump_size(heat_loss_calculation)

        return heat_pump_size

    @staticmethod
    def estimate_peak_kw(
        floor_area_m2: float,
        epc_primary_kwh_per_m2_yr: float | None = None,
        # Prefer these if available:
        space_heat_kwh_per_m2_yr: float | None = None,  # from EPC/SAP if you can
        heat_loss_parameter_W_per_m2K: float | None = None,  # HLP if available
        primary_to_delivered_factor: float = 1.0,
        space_heat_fraction_range=(0.5, 0.75),
        hdd_base_dd: float = 2100.0,  # set per location (base 15.5 °C typical UK)
        t_indoor_C: float = 21.0,
        t_design_ext_C: float = -3.0,
    ):
        ΔT = t_indoor_C - t_design_ext_C

        # 1) Best available path: HLP → direct peak
        if heat_loss_parameter_W_per_m2K is not None:
            peak_kw = heat_loss_parameter_W_per_m2K * floor_area_m2 * ΔT / 1000.0
            return peak_kw, peak_kw  # no range needed

        # 2) Second-best: space-heating demand → HDD method
        if space_heat_kwh_per_m2_yr is not None:
            annual_space_kwh = space_heat_kwh_per_m2_yr * floor_area_m2
            Htot = annual_space_kwh * 1000.0 / (hdd_base_dd * 24.0)  # W/K
            peak_kw = Htot * ΔT / 1000.0
            return peak_kw, peak_kw

        # 3) Minimal inputs: primary energy + assumed fraction → range
        assert epc_primary_kwh_per_m2_yr is not None
        annual_primary = epc_primary_kwh_per_m2_yr * floor_area_m2
        annual_delivered = annual_primary / primary_to_delivered_factor

        def to_peak(space_fraction):
            annual_space = annual_delivered * space_fraction
            Htot = annual_space * 1000.0 / (hdd_base_dd * 24.0)
            return Htot * ΔT / 1000.0

        low = to_peak(space_heat_fraction_range[0])
        high = to_peak(space_heat_fraction_range[1])
        return low, high

    @staticmethod
    def pick_model(peak_kw_range, models_kw=(5, 6, 8.5, 11.2, 14, 17, 20)):
        target = peak_kw_range[1]  # cover the upper end
        for kw in models_kw:
            if kw >= target:
                return kw

        # Return the largest
        return max(models_kw)

    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:
        """

        # Look for a non-intrusive recommendation
        non_intrusive_recommendation = next((
            r for r in self.property.non_invasive_recommendations if r["type"] == "air_source_heat_pump"
        ), {})

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

        # New functions to estimate size of ASHP
        estimated_load = self.estimate_peak_kw(
            floor_area_m2=self.property.floor_area,
            epc_primary_kwh_per_m2_yr=self.property.epc_record.energy_consumption_current,
            primary_to_delivered_factor=1.55,  # use 1.13 if heating fuel is gas
            space_heat_fraction_range=(0.35, 0.60),
            hdd_base_dd=2000.0,  # set from location
            t_indoor_C=21.0,
            t_design_ext_C=-1.0  # set from local CIBSE table
        )
        ashp_size = self.pick_model(estimated_load)

        number_heated_rooms = self._estimate_n_heated_rooms()
        # We now adjust this depending on the floor area to get number of communcal rooms (e.g. hallways)
        communal_heated_rooms = self._estimate_n_communal_heated_rooms()

        ashp_costs = self.costs.air_source_heat_pump(
            ashp_size,
            number_heated_rooms=number_heated_rooms + communal_heated_rooms,
            total_floor_area=self.property.floor_area
        )
        if non_intrusive_recommendation:
            # Update with non-intrusive recommendation
            if non_intrusive_recommendation.get("cost"):
                ashp_costs.update(
                    {"total": non_intrusive_recommendation["cost"], "subtotal": None, "vat": None}
                )

        already_installed = "air_source_heat_pump" in self.property.already_installed

        controls_recommendations = controls_recommender.recommendation
        if already_installed or not controls_recommendations:
            # We set an empty object, so we just produce one recommendation
            controls_recommendations = [None]

        if already_installed:
            ashp_costs = override_costs(ashp_costs)

        if non_intrusive_recommendation and not all([x is None for x in controls_recommendations]):
            # We just use the ttzc control
            controls_recommendations = [
                x for x in controls_recommendations if (
                    x["description_simulation"]["mainheatcont-description"] == "Time and temperature zone control"
                )
            ]

        # This is a map from the heating controls description to the description of the air source heat pump set up
        if ashp_size is None:
            ashp_descriptions = {
                "Time and temperature zone control": (
                    f"Install two cascaded air source heat pumps, and upgrade heating controls to Smart Thermostats, "
                    "room sensors and smart radiator valves (time & temperature zone control). Ensure you have single "
                    "tariff"
                )
            }
        else:

            ashp_descriptions = {
                "Time and temperature zone control": (
                    f"Install a {ashp_size}KW air source heat pump, and upgrade heating controls to Smart Thermostats, "
                    "room sensors and smart radiator valves (time & temperature zone control). Ensure you have a "
                    "single tariff"
                ),
                "Programmer, TRVs and bypass": (
                    f"Install a {ashp_size}KW air source heat pump, with programmer, TRVs and a Bypass valve. Ensure "
                    f"you "
                    "have an 18 or 24 hour tariff"
                ),
            }

        new_heating_description = "Air source heat pump, radiators, electric"
        new_hot_water_description = "From main system"
        ashp_recommendations = []
        for controls_rec in controls_recommendations:

            ashp_costs_with_controls = ashp_costs.copy()

            if controls_rec:
                for key in ashp_costs_with_controls:
                    if ashp_costs_with_controls[key] is not None:
                        ashp_costs_with_controls[key] += controls_rec[key]

            if controls_rec is None:
                description = f"Install a {ashp_size}KW Air source heat pump. Ensure you have a single tariff"
            elif already_installed:
                description = "The property already has an air source heat pump, no further action needed."
            else:
                description = ashp_descriptions[controls_rec["description_simulation"]["mainheatcont-description"]]

            # 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 not non_intrusive_recommendation and self.property.epc_record.tenure not in assumptions.SOCIAL_TENURES:
                if has_cavity_or_loft_recommendations:
                    description = description + (
                        f" You must ensure that the property has an insulated cavity and "
                        f"270mm+ loft insulation to qualify for the grant, to claim £7,500"
                        f" of funding from the boiler upgrade scheme grant. "
                    )
                else:
                    description = description + (
                        f" £7,500 of funding can be claimed from the boiler upgrade scheme"
                    )

            # These are the impacts based on a single tariff with an ashp
            simulation_config = {
                "mainheat_energy_eff_ending": "Good",
                "hot_water_energy_eff_ending": "Average"
            }
            description_simulation = {
                "mainheat-description": new_heating_description,
                "mainheat-energy-eff": simulation_config["mainheat_energy_eff_ending"],
                "hot-water-energy-eff": simulation_config["hot_water_energy_eff_ending"],
                "hotwater-description": new_hot_water_description,
            }
            # 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(new_heating_description).process()
            hotwater_ending_config = HotWaterAttributes(new_hot_water_description).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":
                new_fuel_description = "electricity (not community)"
                fuel_ending_config = MainFuelAttributes(new_fuel_description).process()
                description_simulation = {
                    **description_simulation,
                    "main-fuel": new_fuel_description
                }

            # 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_rec is not None:
                # We should have just the single recommendation for heat controls, which is time
                # and temperature zone controls
                simulation_config = {
                    **simulation_config,
                    **controls_rec["simulation_config"]
                }

                description_simulation = {
                    **description_simulation,
                    **controls_rec["description_simulation"]
                }

            ashp_recommendation = {
                "phase": phase,
                "parts": [],
                "type": "heating",
                "measure_type": "air_source_heat_pump",
                "description": description,
                "starting_u_value": None,
                "new_u_value": None,
                "sap_points": None,
                "already_installed": already_installed,
                "simulation_config": simulation_config,
                "description_simulation": description_simulation,
                **ashp_costs_with_controls,
                "innovation_rate": 0
            }

            ashp_recommendations.append(ashp_recommendation)

        if _return:
            return [ashp_recommendations]

        self.heating_recommendations.extend(ashp_recommendations)

    @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,
        heating_description_simulation,
        costs,
        description,
        phase,
        heating_controls_only,
        system_change,
        system_type,
        heating_product,
        non_intrusive_recommendation=None
    ):
        """
        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 heating_description_simulation: The simulation configuration for the heating description
        :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
        :param system_type: The type of heating system we are recommending
        :param heating_product: The heating product we are recommending, used to determine the system type
        :param non_intrusive_recommendation: A non-intrusive recommendation, which may specify the number of SAP points
                                             or a cost for this recommendation
        """

        if non_intrusive_recommendation is None:
            non_intrusive_recommendation = {}

        # 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_simulation = heating_description_simulation.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"]
                }

                recommendation_description_simulation = {
                    **recommendation_description_simulation,
                    **controls_recommendations[0]["description_simulation"]
                }

                controls_description = controls_recommendations[0]['description']

                recommendation_description = f"{description} {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": [],
                "type": "heating",
                "measure_type": heating_product["type"],
                "description": recommendation_description,
                "starting_u_value": None,
                "new_u_value": None,
                "sap_points": non_intrusive_recommendation.get("sap_points"),
                "already_installed": already_installed,
                **total_costs,
                "simulation_config": recommendation_simulation_config,
                "description_simulation": recommendation_description_simulation,
                # We insert the heating system type here
                "system_type": system_type,
                "survey": check_use_survey(
                    non_intrusive_recommendation, self.property.epc_record.has_been_remodelled
                ),
                # In this instance, we are recommending an entire heating system so the innovation rate is becased
                # on the heating system as whole
                "innovation_rate": heating_product["innovation_rate"],
            }

            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 _estimate_n_heated_rooms(self):
        # 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.epc_record.number_heated_rooms if self.property.epc_record.number_heated_rooms > 0
            else (
                self.property.number_of_rooms - 1 if self.property.number_of_rooms > 1 else
                self.property.number_of_rooms
            )
        )
        # To be conservative, we adjust if we still have 1 room
        if (number_heated_rooms == 1) and (self.property.number_of_rooms > 2):
            number_heated_rooms = self.property.number_of_rooms - 1

        return number_heated_rooms

    def _estimate_n_communal_heated_rooms(self) -> int:
        """
        Estimate number of communal circulation rooms (hallways / landings) that may reasonably contain a heater
        """

        # Base assumptions
        base_by_type = {
            "Flat": 1,
            "Maisonette": 1,
            "Bungalow": 1,
            "House": 2,
        }

        # Fallback if property type unknown
        base = base_by_type.get(self.property.epc_record.property_type, 1)

        # Area-based adjustments
        if self.property.epc_record.property_type in ("Flat", "Maisonette"):
            if self.property.floor_area > 90:
                return base + 1  # duplex or very large flat
            return base

        if self.property.epc_record.property_type == "Bungalow":
            if self.property.floor_area > 100:
                return base + 1  # secondary corridor
            return base

        if self.property.epc_record.property_type == "House":
            if self.property.floor_area > 140:
                return base + 1  # extra landing / circulation
            return base

        return base

    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

        If the property currently has electric storage heaters, with automatic charge control, we allow for a high
        heat retention stoarage heaters recommendation. This is because the automatic charge control is not the same
        as the high heat retention storage heaters. HHR storage heaters aren't guaranteed to be more efficient but
        we can at least present the option to the end user and they can decide if they want to go ahead with the
        recommendation or not. There's a useful guide by quidos, describing the differences between some of the
        different storage heater options:
        https://www.quidos.co.uk/wp-content/uploads/2017/04/Technical-Bulletin-010417-Storage-Heatersv2.pdf

        :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:
            if self.dual_heating:

                controls_prefix = self._map_dual_heating_description(
                    backup_map_to_description="current_controls",
                    output_type="controls_prefix",
                    recommendation_type="hhr"
                )

                if controls_prefix == "current_controls":
                    description_prefix = self.property.main_heating_controls["clean_description"]
                elif controls_prefix == "":
                    description_prefix = ""
                else:
                    raise NotImplementedError("Implement me")
            else:
                description_prefix = ""

            controls_recommender.recommend(
                heating_description="Electric storage heaters",
                description_prefix=description_prefix,
                phase=phase
            )

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

        # We check if there is a high heat retention non-intrusive recommendation
        non_intrusive_recommendation = next(
            (r for r in self.property.non_invasive_recommendations if
             r["type"] == "high_heat_retention_storage_heaters"),
            {}
        )

        # We check if the property has dual heating in place with a boiler and storage heaters
        if self.dual_heating:
            new_heating_description = self._map_dual_heating_description(
                backup_map_to_description="Electric storage heaters",
                output_type="mainheating_description",
                recommendation_type="hhr"
            )
            new_hot_water_description = self.property.hotwater["clean_description"]  # We keep the hot water system
        else:
            new_heating_description = "Electric storage heaters"
            new_hot_water_description = "Electric immersion, off-peak"

        # Set up artefacts, suitable for the simulation and regardless of controls
        heating_ending_config = MainHeatAttributes(new_heating_description).process()
        heating_simulation_config = check_simulation_difference(
            new_config=heating_ending_config, old_config=self.property.main_heating
        )

        hot_water_end_config = HotWaterAttributes(new_hot_water_description).process()
        hot_water_simulation_config = check_simulation_difference(
            new_config=hot_water_end_config, old_config=self.property.hotwater
        )

        heating_simulation_config = {
            **heating_simulation_config,
            **hot_water_simulation_config
        }
        # This upgrade will only take the heating system to average energy efficiency
        if self.property.epc_record.mainheat_energy_eff in ["Very Poor", "Poor", "Average"] and not self.dual_heating:
            heating_simulation_config["mainheat_energy_eff_ending"] = "Good"
        else:
            heating_simulation_config["mainheat_energy_eff_ending"] = self.property.epc_record.mainheat_energy_eff

        # TODO:We possibly shouldn't touch the hot water energy efficiency if we aren't recommending dual immersion
        #      we'll keep this for the moment though
        if self.property.epc_record.hot_water_energy_eff in ["Very Poor", "Poor"]:
            heating_simulation_config["hot_water_energy_eff_ending"] = "Average"
        else:
            heating_simulation_config["hot_water_energy_eff_ending"] = self.property.epc_record.hot_water_energy_eff

        number_heated_rooms = self._estimate_n_heated_rooms()

        # We focus on the 700 watt product
        hhrsh_product = next((x for x in self.hhrsh_products if x["size"] == 700), {})

        # Upgrade to electric storage heaters
        costs = self.costs.high_heat_electric_storage_heaters(
            number_heated_rooms=number_heated_rooms,
            needs_cylinder=self.property.hotwater["system_type"] == "from main system",
            product=hhrsh_product
        )
        if self.dual_heating:
            description = self._map_dual_heating_description(
                backup_map_to_description="Install high heat retention electric storage heaters with an appropriate "
                                          "off-peak tariff.",
                output_type="recommendation_description",
                recommendation_type="hhr"
            )
        else:
            description = "Install high heat retention electric storage heaters with an appropriate off-peak tariff."

        # We check the existing heating system and controls
        if (
            self.property.main_heating["has_electric_storage_heaters"] and
            self.property.main_heating_controls["charging_system"] in
            ["automatic charge control", "manual charge control"]
        ):
            description += (" The current electric heaters may be retrofit with high heat retention storage controls"
                            " however this is dependent on the existing system and may not be possible.")

        heating_description_simulation = {
            "mainheat-description": new_heating_description,
            "mainheat-energy-eff": heating_simulation_config["mainheat_energy_eff_ending"],
            "hotwater-description": new_hot_water_description,
            "hot-water-energy-eff": heating_simulation_config["hot_water_energy_eff_ending"]
        }

        # TODO: Probably don't need to use this for HHRSH - simplify
        recommendations = self.combine_heating_and_controls(
            controls_recommendations=controls_recommender.recommendation,
            heating_simulation_config=heating_simulation_config,
            heating_description_simulation=heating_description_simulation,
            costs=costs,
            description=description,
            phase=phase,
            heating_controls_only=heating_controls_only,
            system_change=system_change,
            system_type="high_heat_retention_storage_heaters",
            non_intrusive_recommendation=non_intrusive_recommendation,
            heating_product=hhrsh_product
        )

        # Check if HHRSH are already installed
        already_installed = "high_heat_retention_storage_heaters" in self.property.already_installed
        for rec in recommendations:
            rec["already_installed"] = already_installed

        if _return:
            return recommendations

        self.heating_recommendations.extend(recommendations)
        return None

    @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

    @staticmethod
    def estimate_electric_boiler_size(num_heated_rooms):
        """
        We use the approach similar to as defined in
        https://www.greenmatch.co.uk/boilers/combi-boilers/electric-combi-boilers
        Instead of radiators as a proxy, we do the number of heated rooms

        :param num_heated_rooms: The number of heated rooms in the property
        :return:
        """

        return max(num_heated_rooms * 1.5, 6)

    def _map_dual_heating_description(
        self, backup_map_to_description, output_type, recommendation_type
    ):
        """
        Utility function to handle dual heating systems
        :param backup_map_to_description:
        :return:
        """

        if backup_map_to_description not in [
            # Recommendation descriptions - these are the textual descriptions shown in the front end
            "Upgrade to a new condensing boiler.",
            "Install high heat retention electric storage heaters with an appropriate off-peak tariff.",
            # Simulation descriptions - this is the new EPC description we simulate with in the case
            # of single heating
            "Boiler and radiators, mains gas",
            "Electric storage heaters",
            # Suffixes allowed
            "",
            # Controls prefixes
            "current_controls"
        ]:
            raise ValueError(f"Invalid backup_map_to_description, given {backup_map_to_description}")

        if output_type not in [
            "recommendation_description",
            "mainheating_description",
            "controls_suffix",
            "controls_prefix",
        ]:
            raise ValueError(f"Invalid output_type, given {output_type}")

        if recommendation_type not in [
            "boiler",
            "hhr",
        ]:
            raise ValueError(f"Given invalid recommendation type {recommendation_type}")

        # "Upgrade to a new condensing boiler."
        if self.dual_heating:

            # We check if we have a mapped description
            if self.property.main_heating["clean_description"] not in self.DUAL_HEATING_DESCRIPTIONS:
                logger.warning(
                    f"We have a dual heating system that hasn't been mapped, defaulting to single "
                    f"{self.property.main_heating['clean_description']}"
                )
                return backup_map_to_description

            return self.DUAL_HEATING_DESCRIPTIONS[
                self.property.main_heating["clean_description"]
            ][recommendation_type][output_type]

        return backup_map_to_description

    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 = {}
        description_simulation = {}

        has_inefficient_space_heating = self.property.epc_record.mainheat_energy_eff in ["Very Poor", "Poor", "Average"]

        # We check if there's a mains connection and the hot water is inefficient, as this will improve with a boiler
        has_inefficient_water = (
            self.property.epc_record.mains_gas_flag and self.property.epc_record.hot_water_energy_eff in ["Very Poor",
                                                                                                          "Poor"]
        )

        non_invasive_recommendation = next((
            r for r in self.property.non_invasive_recommendations if r["type"] == "boiler_upgrade"
        ), {})

        if has_inefficient_space_heating or has_inefficient_water:

            description = self._map_dual_heating_description(
                backup_map_to_description="Upgrade to a new condensing boiler.",
                output_type="recommendation_description",
                recommendation_type="boiler"
            )

            new_heating_eff = (
                "Good" if self.property.epc_record.mainheat_energy_eff in ["Very Poor", "Poor", "Average"]
                else self.property.epc_record.mainheat_energy_eff
            )

            new_hotwater_eff = (
                "Good" if self.property.epc_record.hot_water_energy_eff in ["Very Poor", "Poor", "Average"]
                else self.property.epc_record.hot_water_energy_eff
            )

            simulation_config = {
                "mainheat_energy_eff_ending": new_heating_eff,
                "hot_water_energy_eff_ending": new_hotwater_eff
            }

            description_simulation = {
                "mainheat-energy-eff": simulation_config["mainheat_energy_eff_ending"],
                "hot-water-energy-eff": simulation_config["hot_water_energy_eff_ending"],
            }

            if system_change:
                # Installation of a boiler improves the hot water system so we need to reflect this in
                # the outcome of the recommendation

                new_heating_description = self._map_dual_heating_description(
                    backup_map_to_description="Boiler and radiators, mains gas",
                    output_type="mainheating_description",
                    recommendation_type="boiler"
                )
                new_hotwater_description = "From main system"
                new_fuel_description = "mains gas (not community)"

                heating_ending_config = MainHeatAttributes(new_heating_description).process()
                hotwater_ending_config = HotWaterAttributes(new_hotwater_description).process()
                fuel_ending_config = MainFuelAttributes(new_fuel_description).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,
                }

                description_simulation = {
                    **description_simulation,
                    "mainheat-description": new_heating_description,
                    "hotwater-description": new_hotwater_description,
                    "main-fuel": new_fuel_description
                }

            boiler_costs = self.costs.boiler(
                exising_room_heaters=exising_room_heaters,
                system_change=system_change,
                n_heated_rooms=self.property.epc_record.number_heated_rooms,
                n_rooms=self.property.number_of_rooms
            )

            already_installed = "boiler_upgrade" 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": [],
                "type": "heating",
                "measure_type": "boiler_upgrade",
                "description": description,
                "starting_u_value": None,
                "new_u_value": None,
                "sap_points": non_invasive_recommendation.get("sap_points", None),
                "already_installed": already_installed,
                "simulation_config": simulation_config,
                "description_simulation": description_simulation,
                **boiler_costs,
                "system_type": "boiler_upgrade",
                "survey": check_use_survey(non_invasive_recommendation, self.property.epc_record.has_been_remodelled),
                "innovation_rate": 0,
            }

        # We recommend the heating controls
        # If the property did not previously have a boiler, we combine
        controls_recommender = HeatingControlRecommender(self.property)
        if self.dual_heating:
            description_suffix = self._map_dual_heating_description(
                backup_map_to_description="",
                output_type="controls_suffix",
                recommendation_type="boiler"
            )
        else:
            description_suffix = ""
        controls_recommender.recommend(
            heating_description="Boiler and radiators, mains gas",
            description_suffix=description_suffix,
            phase=recommendation_phase
        )
        # 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 and len(boiler_recommendation):
            # We combine the heating and controls recommendations, in the case of a system change
            # If this is true, we set SAP points to None and survey to False for the boiler recommendation.
            # We check if we actually have a boiler recommendation as we may not if the heating and hot water
            # are already efficient enough

            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,
                    heating_description_simulation=description_simulation,
                    costs=boiler_costs,
                    description=boiler_recommendation["description"],
                    phase=recommendation_phase,
                    heating_controls_only=False,
                    system_change=True,
                    system_type="boiler_upgrade",
                    heating_product={  # temp until we do another product database update
                        "type": "boiler_upgrade",
                        "innovation_rate": 0
                    }
                )
                combined_recommendations.extend(combined_recommendation)

            # Overwrite the existing boiler recommendation
            self.heating_recommendations.extend(combined_recommendations)
        else:
            # We consider a heating control upgrade as a measure which occures in the same phase as a boiler upgrade
            # Namely, we have the following options within this phase
            # 1) Boiler + heating controls
            # 2) Boiler only
            # 3) Heating controls only
            # But they are options that are not mutually exclusive
            # So, we actually set heating controls as a heating recommendation
            self.heating_recommendations.extend(controls_recommender.recommendation)

        return
