import math
from typing import List

import numpy as np
import pandas as pd

from datatypes.enums import QuantityUnits
from backend.Property import Property
from backend.app.plan.schemas import MEASURE_MAP
from BaseUtility import Definitions
from etl.epc_clean.epc_attributes.WallAttributes import WallAttributes
from recommendations.recommendation_utils import (
    r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, update_lowest_selected_u_value,
    get_recommended_part, get_wall_u_value, override_costs, check_simulation_difference,
    check_use_survey
)
from recommendations.config import PARTIALLY_FILLED_PERCENTAGE_ASSUMPTION
from recommendations.Costs import Costs
from recommendations.wall_energy_efficiency_values import cavity_wall_energy_eff, iwi_energy_eff, ewi_energy_eff
from utils.logger import setup_logger

logger = setup_logger()


class WallRecommendations(Definitions):
    YEAR_WALLS_BUILT_WITH_INSULATION = 1990
    # After 1930, Solid brick walls became less populate and instead, cavity walls became a
    # more popular choice
    YEARS_CAVITY_WALLS_BEGAN = 1930
    U_VALUE_UNIT = "w/m-¦k"

    # part L building regulations indicate that any rennovations on an existing property's walls should
    # achieve a U-value of no higher than 0.3
    # This can be seen in table 4.3 in building regulations part L:
    # https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1133079
    # /Approved_Document_L__Conservation_of_fuel_and_power__Volume_1_Dwellings__2021_edition_incorporating_2023_amendments.pdf
    BUILDING_REGULATIONS_PART_L_MAX_U_VALUE = 0.3
    # We don't recommend measures that are too low because it becomes expensive, therefore we aim to avoid
    # diminishing returns. This value should be verified with Osmosis (TODO)
    DIMINISHING_RETURNS_U_VALUE = 0.25

    # Building regulations part L also indicates that cavity wall insulation should result in 0.55 u-value
    BUILDING_REGULATIONS_PART_L_CAVITY_WALL_MAX_U_VALUE = 0.55

    # Part L regulations indicate that any new build should have walls that achieve a u-value of no higher
    # than 0.18.
    BUILDING_REGULATIONS_PART_L_NEW_BUILD_MAX_U_VALUE = 0.18
    # 0.15 is an often cited diminishing returns value for new builds
    NEW_BUILD_DIMINISHING_RETURNS_U_VALUE = 0.15

    # Add some error so that if, for example, a new part we recommend provides a u-value of 0.19,
    # we still consider it as an option
    U_VALUE_ERROR = 0.01

    # Typically when the U-value is around 0.75 and below, and the home is a new build, this is a good indication
    # that the home is already insulated with at least some partial insulation. We don't recommend insulation
    # in this case. This estimate was verified with the Warmfront team and 0.75 has been used as a conservative
    # threshold
    NEW_BUILD_INSULATED = 0.75

    # These are the ending descriptions we consider for walls with external insulation
    # This maps the clean descriptions to the ending descriptions
    EXTERNALLY_INSULATED_WALL_DESCRIPTIONS = {
        "Cavity wall, as built, insulated": "Cavity wall, filled cavity and external insulation",
        "Solid brick, as built, no insulation": "Solid brick, with external insulation",
        "Solid brick, as built, insulated": "Solid brick, with external insulation",
        "Solid brick, as built, partial insulation": "Solid brick, with external insulation",
        "Cob, as built": "Cob, with external insulation",
        "System built, as built, no insulation": "System built, with external insulation",
        'System built, as built, partial insulation': "System built, with external insulation",
        "Granite or whinstone, as built, no insulation": 'Granite or whinstone, with external insulation',
        "Timber frame, as built, no insulation": "Timber frame, with external insulation",
        'Timber frame, as built, partial insulation': 'Timber frame, with external insulation',
        "Sandstone or limestone, as built, no insulation": "Sandstone or limestone, with external insulation",
        "Sandstone or limestone, as built, partial insulation": "Sandstone or limestone, with external insulation",
        "Sandstone, as built, no insulation": "Sandstone, with external insulation",
        "Sandstone, as built, partial insulation": "Sandstone, with external insulation",
    }

    # These are the ending descriptions we consider for walls with internal insulation
    INTERNALLY_INSULATED_WALL_DESCRIPTIONS = {
        "Cavity wall, as built, insulated": "Cavity wall, filled cavity and internal insulation",
        "Solid brick, as built, no insulation": "Solid brick, with internal insulation",
        "Solid brick, as built, insulated": "Solid brick, with internal insulation",
        "Solid brick, as built, partial insulation": "Solid brick, with internal insulation",
        "Cob, as built": "Cob, with internal insulation",
        "System built, as built, no insulation": "System built, with internal insulation",
        'System built, as built, partial insulation': "System built, with internal insulation",
        "Granite or whinstone, as built, no insulation": 'Granite or whinstone, with internal insulation',
        "Timber frame, as built, no insulation": "Timber frame, with internal insulation",
        'Timber frame, as built, partial insulation': 'Timber frame, with internal insulation',
        "Sandstone or limestone, as built, no insulation": "Sandstone or limestone, with internal insulation",
        "Sandstone or limestone, as built, partial insulation": "Sandstone or limestone, with internal insulation",
        "Sandstone, as built, no insulation": "Sandstone, with internal insulation",
        "Sandstone, as built, partial insulation": "Sandstone, with internal insulation",
    }

    def __init__(
        self,
        property_instance: Property,
        materials: List
    ):
        self.property = property_instance
        self.costs = Costs(self.property)
        # For audit purposes, when estimating u values we'll store it
        self.estimated_u_value = None

        # Will contains a list of recommended measures
        self.recommendations = []
        # Contains a list of extended recommendation measures, such as extension insulation
        self.extended_recommendations = []

        self.cavity_wall_insulation_materials = [
            part for part in materials if part["type"] == "cavity_wall_insulation"
        ]

        self.internal_wall_insulation_materials = [
            part for part in materials if part["type"] == "internal_wall_insulation"
        ]

        self.external_wall_insulation_materials = [
            part for part in materials if part["type"] == "external_wall_insulation"
        ]

    def ewi_valid(self):
        """
        This method check available data, to determine if a property is suitable for external wall insulation
        """

        # Current logic: If the property is in a conservation area/heritage building/listed building or a flat,
        # it is not suitable for EWI
        if self.property.restricted_measures or (
            self.property.epc_record.property_type.lower() == "flat"
        ) or (
            self.property.walls['is_cob'] or
            self.property.walls['is_sandstone_or_limestone'] or
            self.property.walls["is_cavity_wall"]
        ):
            return False

        return True

    def is_suitable_for_solid_insulation(self):
        """
        Checks if the wall is of a suitable type for internal/external wall insulation
        """
        if self.property.walls["is_cavity_wall"] or self.property.walls["is_cob"] or self.property.walls[
            "is_granite_or_whinstone"] or self.property.walls["is_sandstone_or_limestone"]:
            return False

        return True

    def recommend(self, phase=0, measures=None, default_u_values=False):
        # if building built after 1990 + we're able to identify U-value +
        # U-value less than 0.18 and if in or close to a conversation area,
        # recommend internal wall insulation as a possible measure

        measures = MEASURE_MAP["wall_insulation"] if measures is None else measures
        if not measures:
            return

        u_value = self.property.walls["thermal_transmittance"]
        u_value = None if pd.isnull(u_value) else u_value

        is_cavity_wall = self.property.walls["is_cavity_wall"]
        insulation_thickness = self.property.walls["insulation_thickness"]

        # We check if the wall is already insulated and if so, we exit
        if (
            (insulation_thickness in ["average", "above average"])
            or self.property.walls["is_filled_cavity"]
            or self.property.walls["clean_description"] in [None, "Sap05:walls"]
        ) and ("cavity_extract_and_refill" not in measures
        ):
            return

        if u_value is not None:

            if self.property.walls["thermal_transmittance_unit"] != self.U_VALUE_UNIT:
                raise NotImplementedError(
                    "Haven't handled the case of other u value units yet"
                )

            # If the property is a new build and the U-value is below 0.75, we don't recommend insulation because it's
            # not practical
            if (self.property.epc_record.transaction_type == "new dwelling") and (
                u_value <= self.NEW_BUILD_INSULATED
            ):
                # Recommend nothing
                return

            # We can't detect it's a cavity wall, but it was built after 1990 so likely built with insulation already
            # + it already has a U-value WORSE than the building regulations, so we recommend either internal or
            # external wall insulation
            if (
                (not is_cavity_wall)
                and (u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE)
            ):
                # Recommend insulation
                self.find_insulation(u_value, phase, measures=measures, default_u_values=default_u_values)
                return

            # We have a sufficiently low U-value
            return

        u_value = get_wall_u_value(
            clean_description=self.property.walls["clean_description"],
            age_band=self.property.age_band,
            is_granite_or_whinstone=self.property.walls["is_granite_or_whinstone"],
            is_sandstone_or_limestone=self.property.walls["is_sandstone_or_limestone"],
        )

        self.estimated_u_value = u_value

        if (is_cavity_wall and "cavity_wall_insulation" in measures) or "cavity_extract_and_refill" in measures:
            if u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
                # Test filling cavity
                self.find_cavity_insulation(u_value, insulation_thickness, phase, measures, default_u_values)

            return

        # Remaining wall types are treated with IWI or EWI
        if (u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) and self.is_suitable_for_solid_insulation():
            self.find_insulation(u_value, phase, measures=measures, default_u_values=default_u_values)
            return

        # If the u-value is within regulations, we don't do anything
        return

    def recommend_extended(self, phase, measures):
        """
        Where we have extended measures, such as extension insulation, which cannot typically be picked up
        from the EPC api, we handle the recommendation of these here
        :param measures:
        :return:
        """

        # These are the measures that are covered by this function
        extended_measures = ["extension_cavity_wall_insulation"]

        measures_to_recommend = [measure for measure in measures if measure in extended_measures]
        if not measures_to_recommend:
            return phase

        # We reset this to be empty
        self.extended_recommendations = []

        recommendation_phase = phase
        for measure in measures_to_recommend:
            if measure == "extension_cavity_wall_insulation":
                recommendation = self.recommend_extension_cavity_wall_insulation(phase=recommendation_phase)
            else:
                raise NotImplementedError(f"Measure {measure} is not implemented")
            recommendation_phase += 1

            self.extended_recommendations.append(recommendation)

        return recommendation_phase

    def recommend_extension_cavity_wall_insulation(self, phase):
        """
        This function produces the recommendation for extension cavity wall insulation
        :return:
        """

        # TODO: We aren't provided with carbon, heat or bill savings figures for this measure

        extension_cavity_insulation_recommendation = [
            r for r in self.property.non_invasive_recommendations if r["type"] == "extension_cavity_wall_insulation"
        ][0]

        # https://surreybuildingprojects.co.uk/how-much-does-a-24m2-extension-cost
        average_extension_floor_area = 24
        # https://assets.publishing.service.gov.uk/media/5f047a01d3bf7f2be8350262
        # /Size_of_English_Homes_Fact_Sheet_EHS_2018.pdf
        # This is rough
        average_house_floor_area = 94

        proposed_extension_floor_area = self.property.floor_area * (
            average_extension_floor_area / average_house_floor_area
        )
        # assume 3 walls are external
        proposed_extension_insulation_wall_area = (
            np.sqrt(proposed_extension_floor_area) * self.property.floor_height * 3
        )

        cost_result = self.costs.cavity_wall_insulation(
            wall_area=proposed_extension_insulation_wall_area,
            material=self.cavity_wall_insulation_materials[0],
        )

        recommendation = {
            "phase": phase,
            "parts": [],
            "type": "extension_cavity_wall_insulation",
            "measure_type": "extension_cavity_wall_insulation",
            "description": "Insulate the cavity walls of the extension",
            "starting_u_value": None,
            "new_u_value": None,
            "sap_points": extension_cavity_insulation_recommendation["sap_points"],
            "heat_demand": 0,
            "kwh_savings": 0,
            "energy_savings": 0,
            "energy_cost_savings": 0,
            "co2_equivalent_savings": 0,
            "already_installed": False,
            "simulation_config": {},
            "description_simulation": {},
            **cost_result,
            "default": True,
        }

        return recommendation

    def find_cavity_insulation(self, u_value, insulation_thickness, phase, measures, default_u_values):
        """
        This method tests different materials to fill the cavity wall, determining which
        material will give us the best U-value.

        We check for diminishing returns, however this function does not check for meeting building
        Part L regulations right now. This is because Part L is less stringent for cavity walls

        Width of a cavity:
        There are various sources online that suggest the width of a cavity wall is around 50mm. The retrofit course
        indicates that most cavities are 50-75mm. Many sources online indicate that 50mm is the standard MINIMUM figure
        therefore we'll use 75mm as the base assumption

        This document:
        https://www.buildingcentre.co.uk/media/_file/pdf/22220_pdf27.pdf
        Indicates that they could be 50-85mm wide

        :param u_value: u_value of the starting wall
        :param insulation_thickness: describes the insulation level of the wall. If "below average", we have a partially
        filled cavity wall
        :param phase: The phase of the recommendation
        :param measures: The measures we're considering
        :param default_u_values: If we should use default u values
        """

        insulation_materials = pd.DataFrame(self.cavity_wall_insulation_materials)
        cavity_width = 75
        if insulation_thickness == "below average":
            cavity_width = cavity_width * (1 - PARTIALLY_FILLED_PERCENTAGE_ASSUMPTION)

        non_invasive_recommendations = next(
            (r for r in self.property.non_invasive_recommendations if
             r["type"] == insulation_materials["type"].values[0]), {}
        )

        # Test the different fill options
        lowest_selected_u_value = None
        recommendations = []
        for _, material in insulation_materials.iterrows():

            part_u_value = r_value_per_mm_to_u_value(
                cavity_width, material["r_value_per_mm"]
            )

            _, new_u_value = calculate_u_value_uplift(u_value, part_u_value)
            new_u_value = math.ceil(new_u_value * 100.0) / 100.0

            if is_diminishing_returns(
                recommendations,
                new_u_value,
                lowest_selected_u_value,
                self.DIMINISHING_RETURNS_U_VALUE,
            ):
                continue

            if new_u_value <= self.BUILDING_REGULATIONS_PART_L_CAVITY_WALL_MAX_U_VALUE:
                lowest_selected_u_value = update_lowest_selected_u_value(
                    lowest_selected_u_value, new_u_value
                )

                is_extraction_and_refill = (
                    "cavity_extract_and_refill"
                    in measures
                )

                cost_result = self.costs.cavity_wall_insulation(
                    wall_area=self.property.insulation_wall_area,
                    material=material.to_dict(),
                    is_extraction_and_refill=is_extraction_and_refill,
                )

                already_installed = (
                    "cavity_wall_insulation" in self.property.already_installed
                )
                if already_installed:
                    cost_result = override_costs(cost_result)

                if is_extraction_and_refill:
                    description = f"Extract and refill cavity wall insulation with {material['description']}"
                else:
                    description = self._make_description(material)

                # updated the new u-value with the best possible our installers have
                if default_u_values:
                    new_u_value = get_wall_u_value(
                        clean_description="Cavity wall, filled cavity",
                        age_band="G",
                        is_granite_or_whinstone=self.property.walls["is_granite_or_whinstone"],
                        is_sandstone_or_limestone=self.property.walls["is_sandstone_or_limestone"],
                    )
                else:
                    new_u_value = max(0.31, new_u_value)

                wall_ending_config = WallAttributes("Cavity wall, filled cavity").process()

                walls_simulation_config = check_simulation_difference(
                    new_config=wall_ending_config, old_config=self.property.walls, prefix="walls_"
                )

                simulation_config = self.set_starting_simulation_config(
                    wall_ending_config=wall_ending_config
                )

                simulation_config = {
                    **simulation_config,
                    **walls_simulation_config,
                    "walls_thermal_transmittance_ending": new_u_value if not default_u_values else 0.7,
                }

                recommendations.append(
                    {
                        "phase": phase,
                        "parts": [
                            get_recommended_part(
                                part=material.to_dict(),
                                quantity=self.property.insulation_wall_area,
                                quantity_unit=QuantityUnits.m2.value,
                                cost_result=cost_result,
                            )
                        ],
                        "type": "cavity_wall_insulation",
                        "measure_type": "cavity_wall_insulation",
                        "description": description,
                        "starting_u_value": u_value,
                        "new_u_value": new_u_value,
                        "sap_points": non_invasive_recommendations.get("sap_points", None),
                        "already_installed": already_installed,
                        "simulation_config": simulation_config,
                        "description_simulation": {
                            "walls-description": "Cavity wall, filled cavity",
                            "walls-energy-eff": "Good"
                        },
                        **cost_result,
                        "survey": check_use_survey(
                            non_invasive_recommendations, self.property.epc_record.has_been_remodelled
                        ),
                        "innovation_rate": material.to_dict()["innovation_rate"]
                    }
                )

        self.recommendations = recommendations

    def get_internal_external_wall_description(self, description_map, new_u_value):

        if "Average thermal transmittance" in self.property.walls["clean_description"]:
            if new_u_value is None:
                raise ValueError("New u value is None")
            return f'Average thermal transmittance {new_u_value} W/m-¦K'

        return description_map[self.property.walls["clean_description"]]

    def set_starting_simulation_config(self, wall_ending_config):
        """
        Helper function to set the starting simulation config
        """

        if wall_ending_config["is_cavity_wall"]:
            efficiency_data = [
                x for x in cavity_wall_energy_eff if
                x["construction-age-band"] == self.property.construction_age_band
            ][0]
        elif wall_ending_config["internal_insulation"]:
            efficiency_data = [
                x for x in iwi_energy_eff if
                x["construction-age-band"] == self.property.construction_age_band
            ][0]
        else:
            efficiency_data = [
                x for x in ewi_energy_eff if
                x["construction-age-band"] == self.property.construction_age_band
            ][0]

        if self.property.epc_record.walls_energy_eff == "Good" and efficiency_data["walls-energy-eff"] not in [
            "Good", "Very Good"
        ]:
            simulation_config = {
                "walls_energy_eff_ending": self.property.epc_record.walls_energy_eff
            }
        elif self.property.epc_record.walls_energy_eff == "Very Good":
            simulation_config = {
                "walls_energy_eff_ending": "Very Good"
            }
        else:
            simulation_config = {
                "walls_energy_eff_ending": efficiency_data["walls-energy-eff"]
            }

        # We check if we have double insulation in any instances
        # TODO: We should pull the energy efficiency categories on double insulation instances, though it's quite rate
        double_insulation = (
            (wall_ending_config["is_filled_cavity"] and wall_ending_config["external_insulation"]) or
            (wall_ending_config["is_filled_cavity"] and wall_ending_config["internal_insulation"]) or
            (wall_ending_config["external_insulation"] and wall_ending_config["internal_insulation"])
        )
        if double_insulation:
            simulation_config["walls_energy_eff_ending"] = "Very Good"

        return simulation_config

    def _find_insulation(self, u_value, insulation_materials, phase, default_u_values):

        lowest_selected_u_value = None
        recommendations = []
        non_invasive_recommendations = next(
            (r for r in self.property.non_invasive_recommendations if
             r["type"] == insulation_materials["type"].values[0]), {}
        )

        for _, insulation_material_group in insulation_materials.groupby("description"):

            for _, material in insulation_material_group.iterrows():

                part_u_value = r_value_per_mm_to_u_value(
                    material["depth"], material["r_value_per_mm"]
                )
                _, new_u_value = calculate_u_value_uplift(u_value, part_u_value)
                new_u_value = math.ceil(new_u_value * 100.0) / 100.0

                # If I have a lowest U value and my new u value is higher than that but lower than the
                # diminishing returns threshold, it can be considered

                # If I have a lowest U value and my new u value is lower than the lowest value, it's
                # further into the diminishing returns threshold and can shouldn't be

                if is_diminishing_returns(
                    recommendations,
                    new_u_value,
                    lowest_selected_u_value,
                    self.DIMINISHING_RETURNS_U_VALUE,
                ):
                    continue

                # We allow a small tolerance for error so we don't discount the recommendation entirely
                if new_u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:

                    lowest_selected_u_value = update_lowest_selected_u_value(
                        lowest_selected_u_value, new_u_value
                    )

                    cost_result = self.costs.solid_wall_insulation(
                        wall_area=self.property.insulation_wall_area,
                        material=material.to_dict(),
                    )

                    already_installed = material["type"] in self.property.already_installed
                    if already_installed:
                        cost_result = override_costs(cost_result)

                    if non_invasive_recommendations.get("cost") is not None:
                        raise NotImplementedError(
                            "Not handled passing costs from non-invasive recommendations for iwi"
                        )

                    if material["type"] == "internal_wall_insulation":
                        new_description = self.get_internal_external_wall_description(
                            self.INTERNALLY_INSULATED_WALL_DESCRIPTIONS, new_u_value
                        )
                    elif material["type"] == "external_wall_insulation":
                        new_description = self.get_internal_external_wall_description(
                            self.EXTERNALLY_INSULATED_WALL_DESCRIPTIONS, new_u_value
                        )
                    else:
                        raise ValueError("Invalid material type")

                    sap_points = non_invasive_recommendations.get("sap_points", None)

                    wall_ending_config = WallAttributes(new_description).process()

                    walls_simulation_config = check_simulation_difference(
                        new_config=wall_ending_config, old_config=self.property.walls, prefix="walls_"
                    )

                    simulation_config = self.set_starting_simulation_config(
                        wall_ending_config=wall_ending_config
                    )

                    simulation_config = {
                        **walls_simulation_config,
                        **simulation_config,
                        "walls_thermal_transmittance_ending": new_u_value
                    }

                    if default_u_values and "Average thermal transmittance" not in new_description:
                        # If we're using default U-values, we overwrite new_u_value
                        new_u_value = get_wall_u_value(
                            clean_description=new_description,
                            age_band=self.property.age_band,
                            is_granite_or_whinstone=self.property.walls["is_granite_or_whinstone"],
                            is_sandstone_or_limestone=self.property.walls["is_sandstone_or_limestone"],
                        )

                    recommendations.append(
                        {
                            "phase": phase,
                            "parts": [
                                get_recommended_part(
                                    part=material.to_dict(),
                                    quantity=self.property.insulation_wall_area,
                                    quantity_unit=QuantityUnits.m2.value,
                                    cost_result=cost_result,
                                )
                            ],
                            "type": material["type"],
                            "measure_type": material["type"],  # This is distinguished between EWI & IWI
                            "description": self._make_description(material),
                            "starting_u_value": u_value,
                            "new_u_value": new_u_value,
                            "already_installed": already_installed,
                            "sap_points": sap_points,
                            "simulation_config": simulation_config,
                            "description_simulation": {
                                "walls-description": new_description,
                                "walls-energy-eff": simulation_config["walls_energy_eff_ending"]
                            },
                            **cost_result,
                            "survey": check_use_survey(
                                non_invasive_recommendations, self.property.epc_record.has_been_remodelled
                            ),
                            "innovation_rate": material.to_dict()["innovation_rate"]
                        }
                    )

        return recommendations

    def find_insulation(self, u_value, phase, measures, default_u_values):
        """
        This function contains the logic for finding potential insulation measures for a property, depending
        on the parts available and whether the property can have external wall insulation installed
        :return:
        """

        # Recommend external and internal wall insulation separately
        # Since external and internal wall insulation are sufficiently different,
        # we separate the logic for for recommending them, therefore we don't
        # consider diminishing returns between the two as they are considered to be separate measures

        prop_already_installed = self.property.already_installed
        # So, we'll end up with problems if e.g. an external wall insulation is already installed and we try and
        # recommend internal wall insulation. To avoid this, we check if either measure is already installed
        # and:
        # 1) If EWI is installed, we don't recommend IWI
        # 2) If IWI is installed, we don't recommend EWI
        # We only produce the recommendation for the moment, for the purpose of re-baselining

        ewi_recommendations = []
        if self.ewi_valid() and "external_wall_insulation" in measures and (
            "internal_wall_insulation" not in prop_already_installed
        ):
            ewi_recommendations = self._find_insulation(
                u_value=u_value,
                insulation_materials=pd.DataFrame(
                    self.external_wall_insulation_materials
                ),
                phase=phase,
                default_u_values=default_u_values
            )

        iwi_recommendations = []
        if "internal_wall_insulation" in measures and "external_wall_insulation" not in prop_already_installed:
            iwi_recommendations = self._find_insulation(
                u_value=u_value,
                insulation_materials=pd.DataFrame(self.internal_wall_insulation_materials),
                phase=phase,
                default_u_values=default_u_values
            )

        self.recommendations += ewi_recommendations + iwi_recommendations

    @staticmethod
    def _make_description(material):
        if material["type"] == "internal_wall_insulation":
            return (
                f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} on internal "
                f"walls"
            )

        if material["type"] == "external_wall_insulation":
            return (
                f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} on external "
                f"walls"
            )

        if material["type"] == "cavity_wall_insulation":
            return f"Fill cavity with {material['description']}"

        raise ValueError("Invalid material type")

    @staticmethod
    def rvalue_per_mm(total_r_value: float, thickness_mm: float) -> float:
        """Return R-value per mm.

        Parameters
        ----------
        total_r_value : float
            Total R-value (in m2K/W).
        thickness_mm : float
            Thickness of the material in mm.

        Returns
        -------
        float
            R-value per mm.
        """
        return total_r_value / thickness_mm

    @staticmethod
    def thermal_conductivity_to_r_value_per_mm(thermal_conductivity: float) -> float:
        """Convert thermal conductivity to R-value per mm.

        Parameters
        ----------
        thermal_conductivity : float
            Thermal conductivity (in W/mK).

        Returns
        -------
        float
            R-value per mm.
        """
        # Calculate R-value in m²K/W for 1 meter of the material
        r_value_per_meter = 1 / thermal_conductivity

        # Convert R-value to R-value per mm
        r_value_per_mm = r_value_per_meter / 1000

        return r_value_per_mm
