import itertools
from utils.logger import setup_logger
from backend.Property import Property
from recommendations.FloorRecommendations import FloorRecommendations
from recommendations.WallRecommendations import WallRecommendations
from recommendations.RoofRecommendations import RoofRecommendations
from recommendations.VentilationRecommendations import VentilationRecommendations
from recommendations.FireplaceRecommendations import FireplaceRecommendations
from recommendations.LightingRecommendations import LightingRecommendations
from recommendations.SolarPvRecommendations import SolarPvRecommendations
from recommendations.WindowsRecommendations import WindowsRecommendations
from recommendations.HeatingRecommender import HeatingRecommender
from recommendations.HotwaterRecommendations import HotwaterRecommendations
from recommendations.SecondaryHeating import SecondaryHeating
from recommendations.Recommendations import Recommendations

logger = setup_logger()


class Mds:
    """
    Handles the contruction of the MDS report
    """

    format_map = {
        "external_wall_insulation": "EWI (Trad Const)",
        "internal_wall_insualtion": "IWI",
        "cavity_wall_insulation": "CWI",
        "loft_insulation": "LI",
        "air_source_heat_pump": "ASHP Htg",
        "high_heat_retention_storage_heaters": "High Heat Retention Storage Heaters",
        "solar_pv": "Solar PV",
    }

    def __init__(self, property_instance: Property, materials, optimise_measures: bool = False):
        self.property_instance = property_instance

        self.floor_recommender = FloorRecommendations(property_instance=property_instance, materials=materials)
        self.wall_recommender = WallRecommendations(property_instance=property_instance, materials=materials)
        self.roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials)
        self.ventilation_recomender = VentilationRecommendations(
            property_instance=property_instance, materials=materials
        )
        self.fireplace_recommender = FireplaceRecommendations(property_instance=property_instance)
        self.lighting_recommender = LightingRecommendations(property_instance=property_instance, materials=materials)
        self.windows_recommender = WindowsRecommendations(property_instance=property_instance, materials=materials)
        self.solar_recommender = SolarPvRecommendations(property_instance=property_instance)
        self.heating_recommender = HeatingRecommender(property_instance=property_instance)
        self.hotwater_recommender = HotwaterRecommendations(property_instance=property_instance)
        self.secondary_heating_recommender = SecondaryHeating(property_instance=property_instance)

        # This flag indicates that we wish to optimise the measures, to the property, depending on the set of measures
        # we have been provided
        self.optimise_measures = optimise_measures

    def select_optimal_measure_set(self, measures):

        # This is the set
        all_considered_measures = [
            'external_wall_insulation',
            'cavity_wall_insulation',
            'loft_insulation',
            'air_source_heat_pump',
            'high_heat_retention_storage_heaters',
            'solar_pv'
        ]

        # Check if our measures are within the ones we've handled
        new = [m for m in measures if m not in all_considered_measures]
        if new:
            raise NotImplementedError("New measures - handle me")

        def prune_options(options, measures):
            options_pruned = []
            for _group in options:
                group_pruned = [m for m in _group if m in measures]
                if not group_pruned:
                    continue
                options_pruned.append(group_pruned)

            return options_pruned

        # For options in here, a property could only possibly have one of these
        one_choice_options = [
            ["external_wall_insulation", "cavity_wall_insulation", "internal_wall_insulation"],
            ["loft_insulation", "flat_roof_insulation", "room_in_roof_insulation"],
            ["solid_floor_insulation", "suspended_floor_insulation"],
        ]
        # prune one_choice_options based on the measure set considered for this property
        one_choice_options_pruned = prune_options(one_choice_options, measures)

        # For options in here, a property could have one or the other so all should be considered
        multi_path_options = [
            ["air_source_heat_pump", "high_heat_retention_storage_heaters", "gas_boiler"]
        ]

        multi_path_options_pruned = prune_options(multi_path_options, measures)

        one_choice_combinations = [list(itertools.product(*one_choice_options_pruned))]
        one_choice_combinations = [list(x) for sublist in one_choice_combinations for x in sublist]
        multi_path_combinations = [list(itertools.product(*multi_path_options_pruned))]
        multi_path_combinations = [list(x) for sublist in multi_path_combinations for x in sublist]

        one_choice_flat = [item for sublist in one_choice_options_pruned for item in sublist]
        multi_path_flat = [item for sublist in multi_path_options_pruned for item in sublist]

        remaining_measures = [
            measure for measure in measures
            if measure not in one_choice_flat and measure not in multi_path_flat
        ]

        # Combine one_choice and multi_path combinations with remaining measures
        final_combinations = []
        for one_choice in one_choice_combinations:
            for multi_path in multi_path_combinations:
                final_combinations.append([m for m in one_choice + multi_path + remaining_measures])

        pruned_combinations = []
        # TODO: We can do these checks once, outside of the loop and prune the combinations
        for combination in final_combinations:
            pruned_measures = []
            for measure in combination:
                if measure not in measures:
                    continue
                # There are certain measures where we need to
                if measure == "external_wall_insulation":
                    # Check if the wall is not cavity since the other wall types can take external wall insulation
                    if (
                        self.wall_recommender.ewi_valid() and
                        not self.property_instance.walls["insulation_thickness"] in ["average", "above average"]
                    ):
                        pruned_measures.append(measure)
                    continue

                if measure == "cavity_wall_insulation":
                    # Check if the wall is cavity
                    if (
                        self.property_instance.walls['is_cavity_wall'] and
                        not self.property_instance.walls['is_filled_cavity']
                    ):
                        pruned_measures.append(measure)
                    continue

                if measure == "loft_insulation":
                    # Check if the roof is suitable for loft insulation and the loft isn't already done
                    # Or, if the home had a u-value for the roof, we don't recommend loft insulation
                    if (
                        self.property_instance.roof["is_pitched"] and
                        not self.roof_recommender.is_loft_already_insulated() and
                        self.property_instance.roof["thermal_transmittance_unit"] is None
                    ):
                        pruned_measures.append(measure)
                    continue

                if measure == "solid_floor_insulation":
                    # Check if the floor is solid
                    if (
                        self.property_instance.floor["is_solid"] and
                        self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] and
                        self.property_instance.floor["thermal_transmittance_unit"] is not None
                    ):
                        pruned_measures.append(measure)
                    continue

                if measure == "suspended_floor_insulation":
                    # Check if the floor is suspended
                    if (
                        self.property_instance.floor["is_suspended"] and
                        self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] and
                        self.property_instance.floor["thermal_transmittance_unit"] is not None
                    ):
                        pruned_measures.append(measure)
                    continue

                if measure == "high_heat_retention_storage_heaters":

                    # For the moment, we recommend storage heaters if the property doesn't already
                    # and don't make it contngent on controls
                    already_has_hhr = self.heating_recommender.is_hhr_already_installed()

                    if (
                        self.heating_recommender.is_high_heat_retention_valid() and
                        not already_has_hhr
                    ):
                        pruned_measures.append(measure)
                    continue

                if measure == "air_source_heat_pump":
                    if self.heating_recommender.is_ashp_valid():
                        pruned_measures.append(measure)
                    continue

                if measure == "solar_pv":
                    if self.solar_recommender.is_solar_pv_valid():
                        pruned_measures.append(measure)
                    continue

                raise NotImplementedError("Implement me")

            if not pruned_measures:
                continue

            pruned_measures_formatted = []
            for pm in pruned_measures:
                pruned_measures_formatted.append({pm: self.format_map[pm]})

            pruned_combinations.append(pruned_measures_formatted)

        # We're left with the subset of measures that are possible for this property
        # These are the possible groups of measures that could be applied to this home

        return pruned_combinations

    def _build(self, measure_config_list, measures):
        not_implemented_measures = [
            "party_wall_insulation",
            "ground_source_heat_pump",
            "shared_ground_loops",
            "communal_heat_networks",
            "district_heating_networks",
            "solar_thermal",
            "draught_proofing",
            "ev_charging",
            "battery",
        ]
        # Check if we have a not implemented measure
        if any([m in not_implemented_measures for m in measure_config_list]):
            raise NotImplementedError("Not implemented measure in the property - implement me")

        mds_recommendations = []
        errors = []
        phase = 0

        # TODO: Could use a decarator to reduce the boilerplate code - insert_recommendation_id and then the append

        if "external_wall_insulation" in measure_config_list:
            recs = self.wall_recommender.mds_recommend_ewi(phase=phase)
            if not recs:
                raise Exception("No recommendations for external wall insulation")
            recs = self.insert_recommendation_id(recs, measures, "external_wall_insulation")
            mds_recommendations.append(recs)
            if self.optimise_measures and len(recs):
                phase += 1

        if "cavity_wall_insulation" in measure_config_list:
            recs = self.wall_recommender.mds_recommend_cavity_wall_insulation(phase=phase)
            recs = self.insert_recommendation_id(recs, measures, "cavity_wall_insulation")
            mds_recommendations.append(recs)
            if self.optimise_measures and len(recs):
                phase += 1

        if "loft_insulation" in measure_config_list:
            # Check if the roof is suitable for loft insulation
            if self.property_instance.roof['is_roof_room']:
                errors.append("Roof is a room")
            else:
                recs = self.roof_recommender.mds_loft_insulation(phase=phase)
                if not recs:
                    raise Exception("No recommendations for loft insulation")
                recs = self.insert_recommendation_id(recs, measures, "loft_insulation")
                mds_recommendations.append(recs)
                if self.optimise_measures and len(recs):
                    phase += 1

        if "internal_wall_insulation" in measure_config_list:
            raise Exception("check me out 4")
            self.wall_recommender.recommend(phase=phase)

        if "suspended_floor_insulation" in measure_config_list:
            raise Exception("check me out 5")
            self.floor_recommender.recommend(phase=phase)

        if "solid_floor_insulation" in measure_config_list:
            raise Exception("check me out 6")
            self.floor_recommender.recommend(phase=phase)

        if "air_source_heat_pump" in measure_config_list:
            recs = self.heating_recommender.recommend_air_source_heat_pump(
                phase=phase, has_cavity_or_loft_recommendations=False, _return=True
            )
            recs = self.insert_recommendation_id(recs, measures, "air_source_heat_pump")
            mds_recommendations.append(recs)
            if self.optimise_measures and len(recs):
                phase += 1

        if "high_heat_retention_storage_heaters" in measure_config_list:
            recs = self.heating_recommender.recommend_hhr_storage_heaters(
                phase=phase, system_change=True, heating_controls_only=False, _return=True
            )
            if recs is None:
                logger.info(
                    f"No recommendations for high heat retention storage heaters, current heating "
                    f"{self.property_instance.main_heating['clean_description']}"
                )
            else:
                recs = self.insert_recommendation_id(recs, measures, "high_heat_retention_storage_heaters")
                mds_recommendations.append(recs)
                if self.optimise_measures and len(recs):
                    phase += 1

        if "low_energy_lighting" in measure_config_list:
            raise Exception("check me out 9")
            self.lighting_recommender.recommend(phase=phase)

        if "cylinder_insulation" in measure_config_list:
            raise Exception("check me out 10")
            self.hotwater_recommender.recommend(phase=phase)

        if "smart_controls" in measure_config_list:
            raise Exception("check me out 11")
            self.heating_recommender.recommend(phase=phase)

        if "zone_controls" in measure_config_list:
            raise Exception("check me out 12")
            self.heating_recommender.recommend(phase=phase)

        if "trvs" in measure_config_list:
            raise Exception("check me out 13")
            self.heating_recommender.recommend(phase=phase)

        if "solar_pv" in measure_config_list:
            recs = self.solar_recommender.mds_recommend(phase=phase, solar_pv_percentage=0.5)
            recs = self.insert_recommendation_id(recs, measures, "solar_pv")
            mds_recommendations.append(recs)
            if self.optimise_measures and len(recs):
                phase += 1

        if "double_glazing" in measure_config_list:
            raise Exception("check me out 15")
            self.windows_recommender.recommend(phase=phase)

        if "mechanical_ventilation" in measure_config_list:
            raise Exception("check me out 16")
            self.ventilation_recomender.recommend(phase=phase)

        if "gas_boiler" in measure_config_list:
            raise Exception("check me out 17")
            self.heating_recommender.recommend(phase=phase)

        if "flat_roof_insulation" in measure_config_list:
            raise Exception("check me out 18")
            self.roof_recommender.recommend(phase=phase)

        if "room_in_roof_insulation" in measure_config_list:
            raise Exception("check me out 19")
            self.roof_recommender.recommend(phase=phase)

        property_representative_recommendations = Recommendations.create_representative_recommendations(
            mds_recommendations, non_invasive_recommendations=[]
        )

        return mds_recommendations, property_representative_recommendations, errors

    def build(self):
        if self.property_instance.measures is None:
            raise NotImplementedError("No measures in the property - implement me")

        if self.optimise_measures:
            measures_set = self.select_optimal_measure_set(self.property_instance.measures)
            mds_recommendations_map = {}
            representative_recommendations_map = {}
            errors_map = {}
            for measures in measures_set:
                measure_config_list = [list(x.keys())[0] for x in measures]
                mds_recommendations, rep_recommendations, errors = self._build(
                    measure_config_list=measure_config_list,
                    measures=measures
                )
                if errors:
                    logger.info(f"Errors: {errors}")

                mds_recommendations_map[str(measure_config_list)] = mds_recommendations
                representative_recommendations_map[str(measure_config_list)] = rep_recommendations
                errors_map[str(measure_config_list)] = errors

            return mds_recommendations_map, representative_recommendations_map, errors_map

        else:
            measure_config_list = [list(m.keys())[0] for m in self.property_instance.measures]
            return self._build(measure_config_list=measure_config_list, measures=self.property_instance.measures)

    @staticmethod
    def insert_recommendation_id(recommendations, measures, measure_name):
        # Insert the recommendation identifier into this recommendation
        measure_config = [m for m in measures if measure_name in m][0]

        idx = 0
        for r in recommendations:
            r["recommendation_id"] = list(measure_config.values())[0] + "-" + str(idx)
            idx += 1

        return recommendations
