import math
from typing import List

import pandas as pd

from BaseUtility import Definitions
from datatypes.enums import QuantityUnits
from backend.app.plan.schemas import MEASURE_MAP
from backend.Property import Property
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_floor_u_value, override_costs, check_simulation_difference, check_use_survey
)
from recommendations.Costs import Costs
from etl.epc_clean.epc_attributes.FloorAttributes import FloorAttributes


class FloorRecommendations(Definitions):
    # 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
    BUILDING_REGULATIONS_PART_L_MAX_U_VALUE = 0.25
    # 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.2

    REGION_LOOKUP = {
        "England and Wales": "England_Wales",
    }

    PART_L_YEAR_CUTOFF = 2002

    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.suspended_floor_insulation_materials = [
            part for part in materials if part["type"] == "suspended_floor_insulation"
        ]

        self.suspended_floor_non_insulation_materials = [
            part for part in materials if part["type"] in [
                "suspended_floor_demolition", "suspended_floor_redecoration", "suspended_floor_vapour_barrier"
            ]
        ]

        # For solid floor, we don't use materials that are too thick
        self.solid_floor_insulation_materials = [
            part for part in materials if part["type"] == "solid_floor_insulation" if float(part["depth"]) <= 75
        ]

        self.solid_floor_non_insulation_materials = [
            part for part in materials if part["type"] in [
                "solid_floor_demolition", "solid_floor_preparation", "solid_floor_vapour_barrier",
                "solid_floor_redecoration"
            ]
        ]

    def recommend(self, phase=0, measures=None):

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

        # If we have no measures or none of the measures are relevant, we can't recommend anything
        if not measures or not any(x in measures for x in MEASURE_MAP["floor_insulation"]):
            return

        if self.property.floor.get("no_data", False):
            return

        u_value = self.property.floor["thermal_transmittance"]
        property_type = self.property.epc_record.property_type
        floor_area = self.property.insulation_floor_area

        if self.property.floor["another_property_below"] | (self.property.floor["insulation_thickness"] in [
            "average", "above average"
        ]):
            # If there's another property below, it's likely impractical to recommend a floor upgrade,
            # or if the floor is already insualted
            return

        # If the property is a flat that isn't at ground level, it's likely impractical to recommend a floor upgrade
        if (self.property.floor_level != 0) and (property_type == "Flat") and (
            self.property.floor["another_property_below"]
        ):
            return

        # If the property is a new build flat, we won't recommend floor upgrades
        if len(self.property.full_sap_epc) and (property_type == "Flat"):
            return

        if u_value:

            # In this case where we have the u-value of a floor, we likely don't have any other information about it
            # so there is no recommendation that we can practically make
            if (
                self.property.floor["is_suspended"] or
                self.property.floor["is_to_unheated_space"] or
                self.property.floor["is_to_external_air"] or
                self.property.floor["is_solid"]
            ):
                raise ValueError("This should not be possible")
            return

        if u_value is None:
            u_value = get_floor_u_value(
                floor_type=self.property.floor_type,
                area=floor_area,
                perimeter=self.property.perimeter,
                age_band=self.property.age_band,
                insulation_thickness=self.property.floor["insulation_thickness"],
                wall_type=self.property.wall_type
            )

            self.estimated_u_value = u_value

        if u_value < self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
            return

        if (
            self.property.floor["is_suspended"] or
            self.property.floor["is_to_unheated_space"] or
            self.property.floor["is_to_external_air"]
        ) and "suspended_floor_insulation" in measures:
            # Given the U-value, we recommend underfloor insulation
            self.recommend_floor_insulation(
                phase=phase,
                u_value=u_value,
                insulation_materials=self.suspended_floor_insulation_materials,
                non_insulation_materials=self.suspended_floor_non_insulation_materials
            )
            return

        if self.property.floor["is_solid"] and "solid_floor_insulation" in measures:
            # Given the U-value, we recommend solid floor insulation options which are usually solid foam
            self.recommend_floor_insulation(
                u_value=u_value,
                insulation_materials=self.solid_floor_insulation_materials,
                non_insulation_materials=self.solid_floor_non_insulation_materials,
                phase=phase
            )
            return

        # In this case, we have no recommendation to make. E.g., if we have a solid floor property
        # but solid floor insulation has been excluded as a measure, we get here
        return

    @staticmethod
    def _make_floor_description(material):

        if material["type"] == "suspended_floor_insulation":
            return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation in "
                    f"suspended floor")

        if material["type"] == "solid_floor_insulation":
            return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation on "
                    f"solid floor")

        if material["type"] == "exposed_floor_insulation":
            return (f"Install {int(material['depth'])}{material['depth_unit']} {material['description']} insulation in "
                    f"exposed floor")

        raise ValueError("Invalid material type - implement me!")

    def recommend_floor_insulation(self, u_value, insulation_materials, non_insulation_materials, phase):
        """
        This method is tasked with estimating the impact of performing suspended floor insulation
        :return:
        """

        insulation_materials = pd.DataFrame(insulation_materials)

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

        lowest_selected_u_value = None
        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 is_diminishing_returns(
                    self.recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE
                ):
                    continue

                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)

                    if material["type"] == "suspended_floor_insulation":
                        cost_result = self.costs.suspended_floor_insulation(
                            insulation_floor_area=self.property.insulation_floor_area,
                            material=material.to_dict(),
                        )

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

                        new_description = "Suspended, insulated"

                    elif material["type"] == "solid_floor_insulation":
                        cost_result = self.costs.solid_floor_insulation(
                            insulation_floor_area=self.property.insulation_floor_area,
                            material=material.to_dict(),
                        )

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

                        new_description = "Solid, insulated"
                    else:
                        raise NotImplementedError("Implement me!")

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

                    floor_ending_config = FloorAttributes(new_description).process()
                    floor_simulation_config = check_simulation_difference(
                        new_config=floor_ending_config, old_config=self.property.floor, prefix="floor_"
                    )

                    simulation_config = {
                        **floor_simulation_config,
                        # We don't simulate the impact using this U-value, but rather the average because this
                        # variable is way too volatile. Will likely be removed from the model
                        "floor_thermal_transmittance_ending": 0.685593,
                    }

                    self.recommendations.append(
                        {
                            "phase": phase,
                            "parts": [
                                get_recommended_part(
                                    part=material.to_dict(),
                                    quantity=self.property.insulation_floor_area,
                                    quantity_unit=QuantityUnits.m2.value,
                                    cost_result=cost_result
                                ),
                            ],
                            "type": material["type"],
                            "measure_type": material["type"],  # This is distinct between suspended and solid floor
                            "description": self._make_floor_description(material),
                            "starting_u_value": u_value,
                            "new_u_value": new_u_value,
                            "sap_points": sap_points,
                            "survey": check_use_survey(
                                non_invasive_recs, self.property.epc_record.has_been_remodelled
                            ),
                            "already_installed": already_installed,
                            "simulation_config": simulation_config,
                            "description_simulation": {
                                "floor-description": "Solid, insulated" if
                                material["type"] == "solid_floor_insulation"
                                else "Suspended, insulated"
                            },
                            **cost_result,
                            "innovation_rate": material["innovation_rate"],
                        }
                    )
