import math
import pandas as pd
from backend.Property import Property
from backend.app.plan.schemas import MEASURE_MAP
from typing import List
from datatypes.enums import QuantityUnits
from recommendations.recommendation_utils import (
    get_roof_u_value, r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns,
    update_lowest_selected_u_value, get_recommended_part, convert_thickness_to_numeric, override_costs,
    check_simulation_difference
)
from recommendations.Costs import Costs
from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes


class RoofRecommendations:
    # part L building regulations indicate that any rennovations on an existing property's roof should
    # achieve a U-value of no higher than 0.16
    # 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.16

    DIMINISHING_RETURNS_U_VALUE = 0.14

    # It is recommended that lofts should have at least 270mm of insulation. If the property has more than 200mm of
    # loft insulation in place already, we do not recommend anything for the moment
    MINIMUM_LOFT_ISULATION_MM = 200
    MINIMUM_RECOMMENDED_LOFT_INSULATION = 280
    # Flat roof should have at least 100mm of insulation
    MINIMUM_FLAT_ROOF_ISULATION_MM = 100

    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 = []

        self.loft_insulation_materials = [
            part for part in materials if (part["type"] == "loft_insulation") and (part["is_installer_quote"])
        ]

        # We don't have proper installer quotes for flat roof insulation
        self.flat_roof_insulation_materials = [
            part for part in materials if part["type"] == "flat_roof_insulation"
        ]

        self.room_roof_insulation_materials = [
            part for part in materials if part["type"] == "room_roof_insulation"
        ]

        # Extract the insulation thickness from the roof, which is used throughout this method
        self.insulation_thickness = convert_thickness_to_numeric(
            self.property.roof["insulation_thickness"],
            self.property.roof["is_pitched"],
            self.property.roof["is_flat"]
        )

    @classmethod
    def get_loft_insulation_sap_limit(cls, roof_energy_eff, existing_thickness):
        """
        Get the SAP limit for loft insulation
        :param roof_energy_eff:
        :return:
        """

        if str(existing_thickness).isdigit():
            if float(existing_thickness) >= 250:
                return 0

        if roof_energy_eff in ["Good", "Very Good"]:
            return 1

        return None

    def is_loft_already_insulated(self, measures):
        """
        Check if the loft is already insulated
        """

        # If we have a non-invasive recommendation for the loft insulation, we can assume that the loft is not insulated
        if "loft_insulation" in measures:
            return False

        return (self.insulation_thickness > self.MINIMUM_LOFT_ISULATION_MM) and self.property.roof["is_pitched"]

    def is_room_roof_insulated_or_unsuitable(self, measures):

        """
        Check if the room roof is already insulated
        """

        # If the roof is a room roof room roof is not included in the measures, we deem the recommendation unsuitable
        unsuitable = "room_roof_insulation" not in measures and self.property.roof["is_roof_room"]
        if unsuitable:
            return True

        full_insulated_room_roof = (
            self.property.roof["is_roof_room"] and
            self.property.roof["insulation_thickness"] in ["average", "above_average"]
        )

        room_roof_insulated_at_rafters = (
            self.property.roof["is_pitched"] and
            self.property.roof["is_at_rafters"] and
            self.property.roof["insulation_thickness"] in ["average", "above_average"]
        )

        has_non_invasive_recommendation = any(
            x["type"] == "room_roof_insulation" for x in self.property.non_invasive_recommendations
        )

        return (full_insulated_room_roof or room_roof_insulated_at_rafters) and not has_non_invasive_recommendation

    def recommend(self, phase, measures=None, default_u_values=False):

        if self.property.roof["has_dwelling_above"]:
            return

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

        u_value = self.property.roof["thermal_transmittance"]

        # If we have a flat roof but we don't have flat roof as a measure, we exit
        if self.property.roof["is_flat"] and "flat_roof_insulation" not in measures:
            return

        # We check if the roof is already insulated and if so, we exit

        # Building regulations part L recommend installing at least 270mm of insulation, however generally we
        # experience diminishing returns in terms of SAP once we go beyond around 150mm of insulation
        # This only holds true for pitched roofs.
        if self.is_loft_already_insulated(measures):
            return

        if (self.insulation_thickness >= self.MINIMUM_FLAT_ROOF_ISULATION_MM) and self.property.roof["is_flat"]:
            return

        if self.is_room_roof_insulated_or_unsuitable(measures):
            return

        if self.property.roof["is_thatched"]:
            return

        # If we have a u-value and we don't have a non-invasive recommendation, we can't recommend anything
        if u_value and not any(
            x in MEASURE_MAP["roof_insulation"] for x in [r["type"] for r in self.property.non_invasive_recommendations]
        ):
            # We don't have enough information to provide a recommendation
            return

        u_value = get_roof_u_value(
            insulation_thickness=self.property.roof["insulation_thickness"],
            has_dwelling_above=self.property.roof["has_dwelling_above"],
            is_loft=self.property.roof["is_loft"],
            is_roof_room=self.property.roof["is_roof_room"],
            is_thatched=self.property.roof["is_thatched"],
            age_band=self.property.age_band,
            is_flat=self.property.roof["is_flat"],
            is_pitched=self.property.roof["is_pitched"],
            is_at_rafters=self.property.roof["is_at_rafters"],
        )

        self.estimated_u_value = u_value
        if (u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) or all(
            m not in measures for m in MEASURE_MAP["roof_insulation"]
        ):
            # The Roof is already compliant
            return

        non_invasive_recommendations = self.property.non_invasive_recommendations

        # We check a specific condition - which will imply loft insulation isn't appropriate but room in roof
        # insulation is
        # 1) We have an uninsulated loft (assumed)
        # 2) We have a non-intrusive recommendation for room in roof insulation

        rir_over_loft = (
            self.property.roof["is_pitched"] and
            self.property.roof["insulation_thickness"] == "none" and
            "room_in_roof_insulation" in [x["type"] for x in non_invasive_recommendations]
        )

        # We firstly handle non-intrusive recommendations, which may override the normal roof insulation recommendations
        if ("loft_insulation" in [x["type"] for x in non_invasive_recommendations]) or (
            self.property.roof["is_pitched"] and "loft_insulation" in measures and
            not self.property.roof["is_at_rafters"]
        ) and not rir_over_loft:
            self.recommend_roof_insulation(
                u_value=u_value,
                insulation_thickness=self.insulation_thickness,
                phase=phase,
                is_flat=False,
                is_pitched=True,
                default_u_values=default_u_values
            )
            return

        if (
            (self.property.roof["is_flat"] and "flat_roof_insulation" in measures) or
            "flat_roof_insulation" in [x["type"] for x in non_invasive_recommendations]
        ):
            self.recommend_roof_insulation(
                u_value=u_value,
                insulation_thickness=0,
                phase=phase,
                is_flat=True,
                is_pitched=False,
                default_u_values=default_u_values
            )
            return

        # There are cases where the property might have a room roof as the second roof, but we have a recommendation for
        # it, so we allow this override
        if self.property.roof["is_roof_room"] and ("room_roof_insulation" in measures) or (
            "room_roof_insulation" in [x["type"] for x in non_invasive_recommendations] or
            rir_over_loft
        ):
            self.recommend_room_roof_insulation(u_value, phase, default_u_values)
            return

        raise NotImplementedError("Implement me")

    @staticmethod
    def make_roof_insulation_description(material):
        if material["type"] == "loft_insulation":
            return f"Install {int(material['depth'])}{material['depth_unit']} of {material['description']} in your loft"

        if material["type"] == "flat_roof_insulation":
            return (
                f"Insulate the home's flat roof with {int(material['depth'])}{material['depth_unit']} of "
                f"{material['description']}"
            )
        if material["type"] == "room_roof_insulation":
            return (f"Insulate your room roof with {int(material['depth'])}{material['depth_unit']} of "
                    f"{material['description']}")

        raise ValueError("Invalid material type")

    def recommend_roof_insulation(
        self, u_value, insulation_thickness, phase, is_pitched, is_flat, default_u_values
    ):

        """
        This method will recommend which insulation materials to use
        This function handles both the case of loft insulation and flat roof insulation

        We also follow advide provided in this article on the Energy Saving Trust website, providing
        high level guidance around roof insulation:
        https://energysavingtrust.org.uk/advice/roof-and-loft-insulation/

        The process roughly looks like the following:
        - Remove the Existing Weatherproof Layer: If the roof is being replaced, remove the old weatherproof layer to
        expose the timber roof surface.
        - Install Insulation Boards: Lay the rigid insulation boards directly on the timber roof surface.
        Ensure the boards fit tightly together to prevent thermal bridging (heat loss through the gaps).
        - Add a Vapour Control Layer (VCL): This is crucial to prevent moisture from entering the insulation layer,
        which can lead to dampness and rot. The VCL is placed over the insulation.
        - Install a New Weatherproof Layer: On top of the insulation and VCL, install a new weatherproof layer. This
        could be traditional roofing materials like bitumen-based felt, rubber membranes like EPDM, or fiberglass.

        :param u_value: U-value of the roof before any retrofit measures have been installed
        :param insulation_thickness: Existing Insulation thickness of the loft
        :param phase: Phase of the recommendation
        :param is_pitched: Is the roof pitched
        :param is_flat: Is the roof flat
        :param default_u_values: Use default u-values
        :return:
        """

        # With loft insulation, 100mm goes between the joists and the rest is rolled on top
        # Therefore the price is 100mm + whatever thickness is rolled on top, rolled at a 90 degree angle
        # from the base layer

        if is_pitched:
            insulation_materials = self.loft_insulation_materials
            measure_type = "loft_insulation"
        elif is_flat:
            insulation_materials = self.flat_roof_insulation_materials
            measure_type = "flat_roof_insulation"
        else:
            raise ValueError("Roof is not pitched or flat")

        if not insulation_materials:
            raise ValueError("No roof insulation materials found")

        insulation_materials = pd.DataFrame(insulation_materials)

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

        lowest_selected_u_value = None
        recommendations = []
        for _, insulation_material_group in insulation_materials.groupby("description"):
            for _, material in insulation_material_group.iterrows():
                # We make sure we hit a depth of 270mm. We should factor in any existing insulation if the
                # loft is already partially insulated.
                # Note: This requirement is only for loft insulation
                if (
                    material["depth"] < self.MINIMUM_RECOMMENDED_LOFT_INSULATION
                ) and is_pitched:
                    continue

                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.loft_and_flat_insulation(
                        floor_area=self.property.insulation_floor_area,
                        material=material
                    )

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

                    if material["type"] == "loft_insulation":
                        # We take the new thickness as just the thickness of the insulation, to be conservative
                        # and assume that any existing insulation will be replaced
                        new_thickness = material["depth"]

                        # This is based on the values we have in the training data
                        valid_numeric_values = [
                            12,
                            25,
                            50,
                            75,
                            100,
                            150,
                            200,
                            250,
                            270,
                            300,
                            350,
                            400,
                        ]

                        proposed_depth = new_thickness
                        if (new_thickness not in valid_numeric_values) and material["type"] == "loft_insulation":
                            # Take the nearest value for scoring
                            proposed_depth = min(
                                valid_numeric_values, key=lambda x: abs(x - proposed_depth)
                            )

                        if proposed_depth >= 300:
                            new_efficiency = "Very Good"
                        else:
                            if self.property.data["roof-energy-eff"] not in ["Good", "Very Good"]:
                                new_efficiency = "Good"
                            else:
                                new_efficiency = "Very Good"

                        new_description = f"Pitched, {int(proposed_depth)}mm loft insulation"

                        if default_u_values:
                            # We update the u-value with the default if we're using default u-values
                            new_u_value = get_roof_u_value(
                                insulation_thickness=str(int(new_thickness)),
                                has_dwelling_above=self.property.roof["has_dwelling_above"],
                                is_loft=self.property.roof["is_loft"],
                                is_roof_room=self.property.roof["is_roof_room"],
                                is_thatched=self.property.roof["is_thatched"],
                                age_band=self.property.age_band,
                                is_flat=self.property.roof["is_flat"],
                                is_pitched=self.property.roof["is_pitched"],
                                is_at_rafters=self.property.roof["is_at_rafters"],
                            )

                    elif material["type"] == "flat_roof_insulation":
                        new_description = "Flat, insulated"
                        new_efficiency = "Good"
                        if default_u_values:
                            new_u_value = get_roof_u_value(
                                insulation_thickness="100",
                                has_dwelling_above=self.property.roof["has_dwelling_above"],
                                is_loft=self.property.roof["is_loft"],
                                is_roof_room=self.property.roof["is_roof_room"],
                                is_thatched=self.property.roof["is_thatched"],
                                age_band=self.property.age_band,
                                is_flat=self.property.roof["is_flat"],
                                is_pitched=self.property.roof["is_pitched"],
                                is_at_rafters=self.property.roof["is_at_rafters"],
                            )
                    else:
                        raise ValueError("Invalid material type")

                    roof_ending_config = RoofAttributes(new_description).process()
                    roof_simulation_config = check_simulation_difference(
                        new_config=roof_ending_config, old_config=self.property.roof, prefix="roof_"
                    )

                    simulation_config = {
                        **roof_simulation_config,
                        "roof_thermal_transmittance_ending": new_u_value,
                        "roof_energy_eff_ending": new_efficiency
                    }

                    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": measure_type,
                            "description": self.make_roof_insulation_description(material),
                            "starting_u_value": u_value,
                            "new_u_value": new_u_value,
                            "sap_points": non_invasive_recommendations.get("sap_points", 0),
                            "already_installed": already_installed,
                            "simulation_config": simulation_config,
                            "description_simulation": {
                                "roof-description": new_description,
                                "roof-energy-eff": new_efficiency
                            },
                            **cost_result,
                            "survey": non_invasive_recommendations.get("survey", False),
                            "innovation_rate": material.to_dict()["innovation_rate"]
                        }
                    )

        self.recommendations = recommendations

    def recommend_room_roof_insulation(self, u_value, phase, default_u_values):
        """
        This method recommends room in roof insulation for properties that have been identified
        to possess a room in roof.

        Because we currently have limited data about the construction of the roof, we make the following
        assumptions:
        1) The room in roof has a sloped roof.
            We will make some basic estimations about the area of the roof given the floor area and the height of the
            floors
        2) Insulation of external walls is covered by the wall recommendation class
        3) We assume a "Gable" roof type

        Further, we recommend internal roof insulation for the room in roof

        The following document contains details around best practices for insulating a room in roof
        https://assets.publishing.service.gov.uk/media/61d727d18fa8f50594b59305/retrofit-room-in-roof-insulation-best
        -practice.pdf
        Of particular interest are the following:

        We also follow advide provided in this article on the Energy Saving Trust website, providing
        high level guidance around roof insulation:
        https://energysavingtrust.org.uk/advice/roof-and-loft-insulation/

        To insulate a warm loft, the following advice is given
        "An alternative way to insulate your loft is to fit rigid insulation boards between and over the rafters.
        Rafters are the sloping timbers that make up the roof itself."

        To then insulate a room roof, the following recommendation is provided:
        "If you want to use your loft as a living space, or it is already being used as a living space,
        then you need to make sure that all the walls and ceilings between a heated room and an unheated space
         are insulated.

         - Sloping ceilings can be insulated in the same way as for a warm roof,
           but with a layer of plasterboard on the inside of the insulation.
         - Vertical walls can be insulated in the same way.
         - Flat ceilings can be insulated like a standard loft.

        :param u_value:  Current u-value of the roof
        :param phase: Phase of the recommendation
        :param default_u_values: Use default u-values
        :return:
        """

        # We have a list of materials that can be used for room roof insulation
        # We will iterate over these materials and recommend them based on the current u-value of the roof
        # and the cost of the materials

        rir_non_invasive_recommendation = next(
            (x for x in self.property.non_invasive_recommendations if x["type"] == "room_in_roof_insulation"), {}
        )

        insulation_materials = pd.DataFrame(self.room_roof_insulation_materials)

        # lowest_selected_u_value = None
        recommendations = []
        for _, material_group in insulation_materials.groupby("description"):
            for material in material_group.itertuples():

                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

                # We allow a small tolerance for error so we don't discount the recommendation entirely

                estimated_cost = (
                    material.total_cost * self.property.insulation_floor_area if
                    rir_non_invasive_recommendation.get("cost") is None else
                    rir_non_invasive_recommendation.get("cost")
                )

                # Could also be Roof room(s), ceiling insulated
                new_descriptin = "Roof room(s), insulated"
                roof_ending_config = RoofAttributes(new_descriptin).process()
                roof_simulation_config = check_simulation_difference(
                    new_config=roof_ending_config, old_config=self.property.roof, prefix="roof_"
                )
                if self.property.data["roof-energy-eff"] in ["Very Poor", "Poor"]:
                    new_efficiency = "Average"
                else:
                    new_efficiency = self.property.data["roof-energy-eff"]

                if default_u_values:
                    new_u_value = get_roof_u_value(
                        insulation_thickness="average",
                        has_dwelling_above=self.property.roof["has_dwelling_above"],
                        is_loft=self.property.roof["is_loft"],
                        is_roof_room=self.property.roof["is_roof_room"],
                        is_thatched=self.property.roof["is_thatched"],
                        age_band=self.property.age_band,
                        is_flat=self.property.roof["is_flat"],
                        is_pitched=self.property.roof["is_pitched"],
                        is_at_rafters=self.property.roof["is_at_rafters"],
                    )

                simulation_config = {
                    **roof_simulation_config,
                    "roof_thermal_transmittance_ending": new_u_value,
                    "roof_energy_eff_ending": new_efficiency
                }

                already_installed = "flat_roof_insulation" in self.property.already_installed
                cost_result = {
                    "total": estimated_cost,
                    "labour_hours": 80,
                    "labour_days": 5,
                }
                if already_installed:
                    cost_result = override_costs(cost_result)

                recommendations.append(
                    {
                        "phase": phase,
                        "parts": [],
                        "type": "room_roof_insulation",
                        "measure_type": "room_roof_insulation",
                        "description": "Insulate room in roof at rafters and re-decorate",
                        "starting_u_value": u_value,
                        "new_u_value": new_u_value,
                        "sap_points": rir_non_invasive_recommendation.get("sap_points", None),
                        "simulation_config": simulation_config,
                        "description_simulation": {
                            "roof-description": new_descriptin,
                            "roof-energy-eff": new_efficiency
                        },
                        **cost_result,
                        "already_installed": already_installed,
                        "survey": rir_non_invasive_recommendation.get("survey", None),
                        "innovation_rate": material.to_dict()["innovation_rate"]
                    }
                )

        self.recommendations = recommendations
