from typing import List

import numpy as np

from backend.Property import Property
from backend.app.plan.schemas import MEASURE_MAP
from etl.epc_clean.epc_attributes.WindowAttributes import WindowAttributes
from recommendations.Costs import Costs
from recommendations.recommendation_utils import override_costs, check_simulation_difference, check_use_survey


class WindowsRecommendations:
    # If the property has existing glazing, we scale down the number of windows that need to be glazed
    COVERAGE_MAP = {
        # If most of the windows have already been glazed, we assume that 2/3 are glazed and 1/2 are remaining to be
        # glazed
        "most": 0.33,
        # If glazing is partial, we assume 50/50 split between glazed and unglazed
        "partial": 0.5,
    }

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

        self.recommendation = []

        self.glazing_material = [
            material for material in materials if material["type"] == "windows_glazing"
        ]

        if len(self.glazing_material) != 1:
            raise ValueError("There should only be one window glazing material")
        self.glazing_material = self.glazing_material[0]

    def recommend(self, measures=None, phase=0):
        """
        This method will recommend the best possible glazing options for a property.

        In order to do this, we need to estimate the number of windows that the home has. This information will be
        stored in the property object, under property.number_of_windows
        :return:
        """

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

        # If we have no windows recs, leave
        if not any(x in measures for x in MEASURE_MAP["windows"]) or "sap05" in self.property.windows[
            "clean_description"].lower():
            return

        if self.property.windows["glazing_type"] in ["triple", "high performance"]:
            # We don't make any recommendations in this case. The property already has outstanding glazing
            return

        # We handle the rare case of not having any windows data
        if self.property.windows["clean_description"] is None:
            return

        if self.property.windows["has_glazing"] & (
            self.property.windows["glazing_coverage"] == "full"
        ):
            return

        # If the property is in a conservation area or is a listed building, it becomes more difficult to install
        # double glazing. Therefore, we don't recommend it. It is still possible but is not practical as it
        # requires planning permission and might require a more expensive window type, such as timber.

        number_of_windows = self.property.number_of_windows

        if "double_glazing" in measures and "secondary_glazing" not in measures:
            is_secondary_glazing = False
        elif "secondary_glazing" in measures and "double_glazing" not in measures:
            is_secondary_glazing = True
        else:
            # If the property currently has some secondary glazing but isn't in a conservation area
            #
            is_secondary_glazing = self.property.restricted_measures and (
                self.property.epc_record.windows_energy_eff in ["Poor", "Very Poor"]
            )

        # We check if the windows are partially insulated but we're recommending double glazing as a complete
        # replacement
        double_glazing_replacement = (
            not is_secondary_glazing and
            # As defined in coverage_map in windows attributes
            self.property.windows["glazing_coverage"] in ["partial", "most"]
        )

        if not number_of_windows:
            raise ValueError("Number of windows not specified")

        # We scale the number of windows based on the proportion of existing glazing
        if self.property.epc_record.multi_glaze_proportion != "":

            if (self.property.windows["clean_description"] == "Some double glazing") and (
                self.property.epc_record.windows_energy_eff == "Very Poor") and (
                self.property.epc_record.multi_glaze_proportion == 100
            ):
                # In this case, we assume all of the dinwos need replacing
                n_windows_scalar = 1
            else:
                n_windows_scalar = 1 - (
                    int(self.property.epc_record.multi_glaze_proportion) / 100
                )
        else:
            n_windows_scalar = self.COVERAGE_MAP.get(
                self.property.windows["glazing_coverage"], 1
            )

        number_of_windows *= n_windows_scalar
        number_of_windows = np.ceil(number_of_windows)

        # Handle edge case - prevent number of windows 0
        number_of_windows = max(1, number_of_windows)

        # We then price the job based on the number of windows that there are
        cost_result = self.costs.window_glazing(
            number_of_windows=number_of_windows,
            material=self.glazing_material,
            is_secondary_glazing=is_secondary_glazing,
        )

        measure_type = "double_glazing" if not is_secondary_glazing else "secondary_glazing"

        already_installed = measure_type in self.property.already_installed

        if already_installed:
            cost_result = override_costs(cost_result)
            description = "The property already has double glazing installed. No further action is required."
        else:
            glazing_type = (
                "secondary glazing" if is_secondary_glazing else "double glazing"
            )
            if self.property.windows["glazing_coverage"] in ["partial", "most"] and not double_glazing_replacement:
                description = f"Install {glazing_type} to the remaining windows"
            else:
                description = f"Install {glazing_type} to all windows"

            if self.property.is_listed:
                description += (
                    ". Secondary glazing recommended due to listed building status"
                )
            elif self.property.is_heritage:
                description += (
                    ". Secondary glazing recommended due to herigate building status"
                )
            elif self.property.in_conservation_area:
                description += (
                    ". Secondary glazing recommended due to conservation area status"
                )

        # Set up the simulation config
        windows_energy_eff = "Good"
        if self.property.windows["glazing_type"] == "multiple":
            glazing_type_ending = "multiple"
            glazed_type_ending = (
                "secondary glazing" if is_secondary_glazing else "double glazing installed during or after 2002"
            )
            new_windows_description = "Multiple glazing throughout"

        elif self.property.windows["glazing_type"] == "single":
            # We will only recommend either secondary or double glazing
            glazing_type_ending = (
                "secondary" if is_secondary_glazing else "double"
            )
            glazed_type_ending = (
                "secondary glazing" if is_secondary_glazing else "double glazing installed during or after 2002"
            )

            if is_secondary_glazing:
                new_windows_description = "Full secondary glazing"
            else:
                new_windows_description = "Fully double glazed"

        elif self.property.windows["glazing_type"] == "double":
            glazing_type_ending = (
                "multiple" if is_secondary_glazing else "double"
            )

            # We set glazed type depending on which window type is more prevalent. Since there is already double
            # glazing in place, if we're recommending more double glazing, we set the glazed type to double glazing
            # otherwise, if we're recommending secondary glazing and the proportion of glazing in place already that
            # is double is less than 50% we set the glazed type to secondary glazing

            if not is_secondary_glazing:
                glazed_type_ending = "double glazing installed during or after 2002"
                new_windows_description = "Fully double glazed"
            else:
                if self.property.epc_record.multi_glaze_proportion < 50:
                    glazed_type_ending = "secondary glazing"
                else:
                    glazed_type_ending = "double glazing installed during or after 2002"

                new_windows_description = "Multiple glazing throughout"

        elif self.property.windows["glazing_type"] == "secondary":
            glazing_type_ending = (
                "secondary" if is_secondary_glazing else "multiple"
            )
            # This is the opposite. If there is secondary glazing in place, and we're recommending double
            # we set glazed_type_ending, depending on the proportion of glazing in place
            if is_secondary_glazing:
                glazed_type_ending = "secondary glazing"
                new_windows_description = "Full secondary glazing"
            else:
                if self.property.epc_record.multi_glaze_proportion < 50:
                    glazed_type_ending = "double glazing installed during or after 2002"
                else:
                    glazed_type_ending = "secondary glazing"
                new_windows_description = "Multiple glazing throughout"
            # Windows only end up with an average efficiency
            windows_energy_eff = "Average"

        else:
            raise ValueError("Invalid glazing type - implement me")

        if self.property.epc_record.windows_energy_eff == "Very Good":
            windows_energy_eff = "Very Good"

        # For post 2002 windows, the energy efficiency is "Good" and so for the simulation, we simulate with "Good"
        windows_ending_config = WindowAttributes(new_windows_description).process()

        windows_simulation_config = check_simulation_difference(
            new_config=windows_ending_config, old_config=self.property.windows, prefix="windows_"
        )

        simulation_config = {
            **windows_simulation_config,
            "multi_glaze_proportion_ending": 100,
            "windows_energy_eff_ending": windows_energy_eff,
            "glazing_type_ending": glazing_type_ending,
            "glazed_type_ending": glazed_type_ending,
        }

        description_simulation = {
            "multi-glaze-proportion": 100,
            "windows-energy-eff": windows_energy_eff,
            "windows-description": new_windows_description,
            "glazed-type": glazed_type_ending,
        }

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

        self.recommendation = [
            {
                "phase": phase,
                "parts": [],
                "type": "windows_glazing",
                "measure_type": measure_type,
                "description": description,
                "starting_u_value": None,
                "new_u_value": None,
                "sap_points": non_invasive_recommendation.get("sap_points", None),
                "already_installed": already_installed,
                **cost_result,
                "is_secondary_glazing": is_secondary_glazing,
                "description_simulation": description_simulation,
                "simulation_config": simulation_config,
                "survey": check_use_survey(
                    non_invasive_recommendation, self.property.epc_record.has_been_remodelled
                ),
                "innovation_rate": self.glazing_material["innovation_rate"],
            }
        ]

    def recommend_mixed_glazing(self, phase):
        """
        This function will recommend mixed glazing to the property. This is a more specific recommendation than
        the general windows recommendation, but is almost certain to arise from a survey
        :return:
        """

        mixed_glazing_recommendation_config = next(
            (r for r in self.property.non_invasive_recommendations if r["type"] == "mixed_glazing"), {}
        )
        if not mixed_glazing_recommendation_config:
            return

        description = (
            "Install a combination of secondary and double glazing to single glazed windows" if
            not mixed_glazing_recommendation_config.get("description")
            else mixed_glazing_recommendation_config["description"]
        )

        windows_ending_config = WindowAttributes("Full secondary glazing").process()

        windows_simulation_config = check_simulation_difference(
            new_config=windows_ending_config, old_config=self.property.windows, prefix="windows_"
        )

        windows_simulation_config = {
            **windows_simulation_config,
            "windows_energy_eff_ending": "Average",
            "glazed_type_ending": "secondary glazing",
            "multi_glaze_proportion_ending": 100,
        }

        return [
            {
                "phase": phase,
                "parts": [],
                "type": "mixed_glazing",
                "measure_type": "mixed_glazing",
                "description": description,
                "starting_u_value": None,
                "new_u_value": None,
                "already_installed": False,
                "sap_points": mixed_glazing_recommendation_config["sap_points"],
                "heat_demand": None,  # We will predict this
                "kwh_savings": None,  # We will predict this
                "co2_equivalent_savings": None,  # We will predict this
                "energy_cost_savings": None,  # We will predict this
                "total": mixed_glazing_recommendation_config["cost"],
                # We use a very simple and rough estimate of 4 hours per unit
                "labour_hours": mixed_glazing_recommendation_config.get("labour_hours", 8),
                "labour_days": mixed_glazing_recommendation_config.get("labour_days", 1),  # Assume 8 hour day
                "survey": mixed_glazing_recommendation_config["survey"],
                "simulation_config": windows_simulation_config,
                "description_simulation": {
                    "multi-glaze-proportion": 100,
                    "windows-energy-eff": "Average",
                    "windows-description": "Multiple glazing throughout",
                    "glazed-type": "secondary glazing",
                },
            }
        ]
