from recommendations.recommendation_utils import convert_thickness_to_numeric
from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes
from etl.epc_clean.epc_attributes.WallAttributes import WallAttributes
from etl.epc_clean.epc_attributes.FloorAttributes import FloorAttributes


class Eligibility:
    """
    Given the epc data about a property, this class holds the logic for determining if the home
    is eligible for a specific retrofit measure.

    For example, this could be whether the loft has insulation below a standardised threshold, or
    if it has an empty cavity

    Further to this, this class is responsible for determining if the property is suitable for specific funding
    schemes
    """

    loft = None
    cavity = None
    solid_wall = None
    room_roof = None
    flat_roof = None
    suspended_floor = None
    solid_floor = None

    # schemes based on Warmfront now
    gbis_warmfront = None
    eco4_warmfront = None
    # Schemes based on full eligibility
    gbis = None
    eco4 = None

    # If the loft has less than 100mm of insulation, we classify the home has needing loft insulation
    LOFT_INSULATION_THRESHOLD = 100
    HIGH_LOFT_INSULATION_THRESHOLD = 269

    # Because EPCS have different values for tenure, we need to remap them to a common set of values
    tenure_remap = {
        'NO DATA!': "unknown",
        'Not defined - use in the case of a new dwelling for which the intended tenure in not known. It is no':
            "unknown",
        'Owner-occupied': 'Owner-occupied',
        'Rented (private)': 'Rented (private)',
        'Rented (social)': 'Rented (social)',
        'owner-occupied': 'Owner-occupied',
        'rental (private)': 'Rented (private)',
        'rental (social)': 'Rented (social)',
        'unknown': "unknown",
    }

    def __init__(self, epc, cleaned):
        self.epc = epc
        self.cleaned = cleaned

        self.walls = self.parse_fabric("walls-description")
        self.roof = self.parse_fabric("roof-description")
        self.floor = self.parse_fabric("floor-description")

        self.tenure = self.tenure_remap.get(self.epc["tenure"], None)

    def parse_fabric(self, key):

        # Get the cleaned version of the description
        remapped = [
            data for data in self.cleaned[key] if
            data["original_description"] == self.epc[key]
        ]
        if remapped:
            return remapped[0]

        if "SAP05:" in self.epc[key]:
            # This is a placeholder method for handling this but this will occur in the case of a very old
            # EPC and therefore we just skip
            self.epc[key] = "(assumed)"

        if key == "walls-description":
            cleaner_cls = WallAttributes(self.epc[key])

        elif key == "roof-description":
            cleaner_cls = RoofAttributes(self.epc[key])

        elif key == "floor-description":
            cleaner_cls = FloorAttributes(self.epc[key])

        else:
            raise ValueError("Invalid key")
        output = cleaner_cls.process()
        output["clean_description"] = cleaner_cls.description.replace("(assumed)", "").rstrip().capitalize()

        return output

    def loft_insulation(self, loft_thickness_threshold: int = None):
        """
        Given the description of roof, this function determines whether or not the property is suitable for loft
        insulation. A loft existing insulation with a thickness below loft_thickness_threshold, is deemed to
        be suitable for loft insulation
        :param loft_thickness_threshold: Integer, Optional. If provided, any loft found with insulation lower than
                                         this thickness is deemed to be suitable for loft insulation. If this
                                         parameter is not provided, this method will default to the variable specified
                                         in LOFT_INSULATION_THRESHOLD
        """

        loft_thickness_threshold = (
            self.LOFT_INSULATION_THRESHOLD if loft_thickness_threshold is None else loft_thickness_threshold
        )

        high_loft_thickness_threshold = self.HIGH_LOFT_INSULATION_THRESHOLD

        # We firstly check if the roof is a loft
        is_loft = self.roof["is_pitched"] and (not self.roof["is_roof_room"])

        if not is_loft:
            self.loft = {
                "suitability": False,
                "thickness": None,
                "reason": "roof not loft",
                "thickness_classification": None
            }
            return

        # If it is a loft, we'll convert the textual thickenss to a numerical value we can easily use
        insulation_thickness = convert_thickness_to_numeric(
            string_thickness=self.roof["insulation_thickness"],
            is_pitched=self.roof["is_pitched"],
            is_flat=self.roof["is_flat"]
        )

        if insulation_thickness <= 100:
            thickness_classification = "0-100mm"
        elif insulation_thickness <= high_loft_thickness_threshold:
            thickness_classification = "100-270mm"
        else:
            thickness_classification = "270mm+"

        if insulation_thickness <= loft_thickness_threshold:
            # We produce a thiclkness classification for the loft
            # 0 - 100mm insulation
            # 100 - 270mm insulation
            # 270mm+ insulation

            self.loft = {
                "suitability": True,
                "thickness": insulation_thickness,
                "reason": None,
                "thickness_classification": thickness_classification
            }
            return

        # Insulation is already thick enough
        self.loft = {
            "suitability": False,
            "thickness": insulation_thickness,
            "reason": "existing insulation",
            "thickness_classification": thickness_classification
        }
        return

    def cavity_insulation(self):

        """
        Given the description of the walls, this function determines if the property is suitable for cavity wall
        insulation
        :return:
        """

        is_cavity = self.walls["is_cavity_wall"]
        is_empty = (not self.walls["is_filled_cavity"])
        is_as_built = (
            self.walls["is_as_built"] and self.walls["insulation_thickness"] not in ["average", "above average"]
            and self.walls["is_assumed"]
        )
        is_partial_filled = "partial" in self.walls["clean_description"].lower()
        # We look for potentially under performing cavities - anything that is assumed, as built and insulated
        is_underperforming = (
            self.walls["is_as_built"] and self.walls["insulation_thickness"] in ["average"] and self.walls["is_assumed"]
        )

        is_unfilled_cavity = is_cavity and (is_empty and not is_partial_filled)
        is_partial_filled_cavity = is_cavity and is_partial_filled
        is_assumed_filled_cavity = is_cavity and is_as_built
        is_underperforming_cavity = is_cavity and is_underperforming

        # Check if it has internal or external wall insulation
        has_internal_wall_insulation = self.walls["internal_insulation"]
        has_external_wall_insulation = self.walls["external_insulation"]

        if has_internal_wall_insulation or has_external_wall_insulation:
            self.cavity = {
                "suitability": False,
                "type": "internal or external wall insulation"
            }
            return

        if is_unfilled_cavity:
            self.cavity = {
                "suitability": True,
                "type": "empty",
            }
            return

        if is_assumed_filled_cavity:
            self.cavity = {
                "suitability": True,
                "type": "as built assumed",
            }
            return

        if is_partial_filled_cavity:
            self.cavity = {
                "suitability": True,
                "type": "partial"
            }
            return

        if is_underperforming_cavity:
            self.cavity = {
                "suitability": True,
                "type": "underperforming"
            }
            return

        self.cavity = {
            "suitability": False,
            "type": "full"
        }

    def solid_wall_insulation(self):
        """
        Given the description of the walls, this function determines if the property is suitable for solid wall
        insulation
        :return:
        """

        is_solid = self.walls["is_solid_brick"]
        is_insulated = self.walls["insulation_thickness"] in ["average", "above average"]

        if is_solid and is_insulated:
            self.solid_wall = {
                "suitability": True,
            }
            return

        self.solid_wall = {
            "suitability": False,
        }

    def room_roof_insulation(self):
        is_room_roof = self.roof["is_roof_room"]

        if not is_room_roof:
            self.room_roof = {
                "suitability": False,
                "thickness": None
            }
            return

        insulation_thickness = convert_thickness_to_numeric(
            self.roof["insulation_thickness"],
            self.roof["is_pitched"],
            self.roof["is_flat"]
        )

        self.room_roof = {
            "suitability": is_room_roof and insulation_thickness == 0,
            "thickness": insulation_thickness
        }

    def flat_roof_insulation(self):
        is_flat = self.roof["is_flat"]

        if not is_flat:
            self.flat_roof = {
                "suitability": False,
                "thickness": None
            }
            return

        insulation_thickness = convert_thickness_to_numeric(
            self.roof["insulation_thickness"],
            self.roof["is_pitched"],
            self.roof["is_flat"]
        )

        self.flat_roof = {
            "suitability": is_flat and insulation_thickness <= 100,
            "thickness": insulation_thickness
        }

    def suspended_floor_insulation(self):

        if "no_data" in self.floor.keys():
            if self.floor["no_data"]:
                self.suspended_floor = {
                    "suitability": False,
                }
                return

        is_suspended = self.floor["is_suspended"]
        is_insulated = self.floor["insulation_thickness"] in ["average", "above average"]

        self.suspended_floor = {
            "suitability": is_suspended and (not is_insulated),
        }
        return

    def solid_floor_insulation(self):

        if "no_data" in self.floor.keys():
            if self.floor["no_data"]:
                self.solid_floor = {
                    "suitability": False,
                }
                return

        is_solid = self.floor["is_solid"]
        is_insulated = self.floor["insulation_thickness"] in ["average", "above average"]

        self.solid_floor = {
            "suitability": is_solid and (not is_insulated),
        }
        return

    def check_gbis_warmfront(self):
        """
        The Eligibility criteria for the Great British Insulation Scheme (GBIS) can be found here:
        https://www.ofgem.gov.uk/environmental-and-social-schemes/great-british-insulation-scheme/homeowners-and-tenants

        At a high level, the criteria is the following:
        - The home must be within council tax bands A-D in England, A-E in Scotland, A-E in Wales
        - It must have an EPC rating of D or below

        For the moment, we won't check whether a property is in the correct council tax band. There is likely
        to be public data for this since there is a govenment website which allows you to search for properties:
        https://www.gov.uk/council-tax-bands
        This data is possibly contained on the council tax valuation list but it remains to be see (seems unlikely)
        whether or not the data is openly accessible
        https://www.gov.uk/government/statistics/quality-assurance-of-administrative-data-in-the-uk-house-price-index
        /valuation-office-agency-council-tax-valuation-lists

        Currently, we tailor this module to the Warmfront Team and their delivery capabilities (both practically and
        commercially). Therefore, we will check:
        1) Whether the property is an EPC D or below
        2) Whether the property is suitible for cavity wall insulation

        However, GBIS applies to many insulation measures, which can be seen in the ofgem document

        GBIS does not have any minimum upgrade requirement so we don't need to simulate the post retrofit sap score
        using the machine learning model
        """

        # Check if the property is suitable for cavity wall
        self.cavity_insulation()

        current_sap = int(self.epc["current-energy-efficiency"])
        # We have a strict suitability check and a non-strict check

        # Perfect strictness
        if (self.cavity["type"] == "empty") and (current_sap < 69):
            self.gbis_warmfront = {
                "eligible": True,
                "strict": True,
                "message": "Perfect suitability",
            }
            return

        # Near perfect
        if self.cavity["suitability"] and (current_sap < 69):
            self.gbis_warmfront = {
                "eligible": True,
                "strict": True,
                "message": "Near perfect suitability",
            }
            return

        self.gbis_warmfront = {
            "eligible": False,
            "strict": False,
            "message": "All conditions fail",
        }

    def check_eco4_warmfront(self):
        """
        This funciton will check if the property is eligible for funding under the ECO4 scheme

        For the moment, this function will consider just measures that can be implemented by the
        Warmfront team, therefore we will only check if a property has an uninsulated loft AND uninsulated
        cavity

        We use Ofgem's V1.1 ECO 4 guidance document for the conditions under which a property is elligible
        This document can be found here:
        https://www.ofgem.gov.uk/sites/default/files/2023-02/ECO4%20Delivery%20Guidance%20v1.1%20%281%29.pdf

        The conditions (to be reviewed) to be eligible for retrofit, under ECO4, are the following:
        1) The property is a social home (This is assumed prior to this function as this code will often
        be run on property lists provided by a HA
        2) The property is an EPC E or below
        3) The property has an unfilled cavity and uninsulated loft
        4) After retrofit, the property will hit an EPC C

        Note: This criteria will likely be adjusted depending on the properties that can be served right now

        If the post_retrofit_sap is provided, then is this value is 69 or higher, the property will be deemed
        to be eligible for ECO4 funding. If the post_retrofit_sap is not provided, the property will be
        deemed to be eligible, conditional to the post_retrofit_sap score check
        :param post_retrofit_sap:
        :return:
        """

        current_sap = int(self.epc["current-energy-efficiency"])
        self.cavity_insulation()
        self.loft_insulation()

        # We put in a placeholder when the roof is not a loft
        if self.loft["reason"] == "roof not loft":
            self.loft["thickness"] = 999

        # Case 1: No conditions meet
        if not self.cavity["suitability"] and (self.loft["thickness"] > 100) and current_sap >= 55:
            self.eco4_warmfront = {
                "eligible": False,
                "strict": False,
                "message": "All conditions fail",
                "cavity_type": self.cavity["type"],
                "loft_type": self.loft["thickness_classification"]
            }
            return

        # Case 2 - perfect match
        if (self.cavity["type"] == "empty") and (self.loft["thickness"] <= 100) and (current_sap < 55):
            self.eco4_warmfront = {
                "eligible": True,
                "strict": True,
                "message": "Perfect suitability",
                "cavity_type": self.cavity["type"],
                "loft_type": self.loft["thickness_classification"]
            }
            return

        # Case 2.5 - near perfect match - but we would not recommend this using the model
        if self.cavity["suitability"] and (self.loft["thickness"] <= 100) and (current_sap < 55):
            self.eco4_warmfront = {
                "eligible": True,
                "strict": True,
                "message": "Near perfect suitability",
                "cavity_type": self.cavity["type"],
                "loft_type": self.loft["thickness_classification"]
            }
            return

        # Case 3 - cavity is suitable, loft is within 150mm, sap is good
        if self.cavity["suitability"] and (self.loft["thickness"] <= 150) and (current_sap < 55):
            self.eco4_warmfront = {
                "eligible": True,
                "strict": False,
                "message": "Meets cavity, loft borderline, meets sap",
                "cavity_type": self.cavity["type"],
                "loft_type": self.loft["thickness_classification"]
            }
            return

        # Case 3 - cavity is suitable, loft is not, sap is good
        if self.cavity["suitability"] and (self.loft["thickness"] > 150) and (current_sap < 55):
            self.eco4_warmfront = {
                "eligible": True,
                "strict": False,
                "message": "Meets cavity and sap",
                "cavity_type": self.cavity["type"],
                "loft_type": self.loft["thickness_classification"]
            }
            return

        # Case 4 - cavity is not suitable, loft is, sap is not - we say this is not elifible
        if not self.cavity["suitability"] and (self.loft["thickness"] <= 100) and (current_sap < 55):
            self.eco4_warmfront = {
                "eligible": False,
                "strict": False,
                "message": "failed fabric check",
                "cavity_type": self.cavity["type"],
                "loft_type": self.loft["thickness_classification"]
            }
            return

        # Case 5 - cavity and loft suitable, sap too high
        if self.cavity["suitability"] and (self.loft["thickness"] <= 150) and (current_sap >= 55):
            self.eco4_warmfront = {
                "eligible": True,
                "strict": False,
                "message": "Meets fabric, fails SAP check",
                "cavity_type": self.cavity["type"],
                "loft_type": self.loft["thickness_classification"]
            }
            return

        # Case 6 - meets just cavity
        if self.cavity["suitability"] and (self.loft["thickness"] > 100) and (current_sap >= 55):
            self.eco4_warmfront = {
                "eligible": True,
                "strict": False,
                "message": "Meets just cavity",
                "cavity_type": self.cavity["type"],
                "loft_type": self.loft["thickness_classification"]
            }
            return

        # Case 7 - fails cavity, loft but meets sap
        if not self.cavity["suitability"] and (self.loft["thickness"] > 100) and (current_sap < 55):
            self.eco4_warmfront = {
                "eligible": False,
                "strict": False,
                "message": "Fails cavity and loft, meets SAP",
                "cavity_type": self.cavity["type"],
                "loft_type": self.loft["thickness_classification"]
            }
            return

        # Case 8 - fails cavity, meets loft, fails sap
        if not self.cavity["suitability"] and (self.loft["thickness"] <= 100) and (current_sap >= 55):
            self.eco4_warmfront = {
                "eligible": False,
                "strict": False,
                "message": "Fails cavity, meets loft, fails SAP",
                "cavity_type": self.cavity["type"],
                "loft_type": self.loft["thickness_classification"]
            }
            return

        raise ValueError("Implement me")

    def check_gbis(self):

        """
        The Eligibility criteria for the Great British Insulation Scheme (GBIS) can be found here:
        https://www.ofgem.gov.uk/environmental-and-social-schemes/great-british-insulation-scheme/homeowners-and-tenants

        Full delivery guidance and be downloaded here:
        https://www.ofgem.gov.uk/sites/default/files/2023-08/Great%20British%20Insulation%20Scheme%20Delivery
        %20Guidance%20V101693416860968.pdf

        For social housing, the criteria is the following:

        If the property is currently an EPC D:
        - It's valid for innovation measures only but not a heating control measure
        - The property must be rented at below the market rate. All eligible social housing is treated based on the
          low income group, therefore the tennant must be in receipt of one the eligible benefits

        If the property is currently an EPC E or below:
        - It's valid for all eligible insulation measures
        - The property must be rented at below the market rate. All eligible social housing is treated based on the
          low income group, therefore the tennant must be in receipt of one the eligible benefits

        From GBIS guidance document:
        Determining whether the premises are let below market rate

        3.101 Social housing under this provision will only be eligible where the housing is let below
        the market rate. The supplier must produce a declaration signed by a social landlord
        providing confirmation that the social housing premises are let below the market rate,
        or where the premises are currently void, have previously and will be let below the
        market rate. The declaration to be signed by a social landlord is included within the
        Eligibility and Pre-Retrofit Declaration form. This declaration form must be retained by
        suppliers and be available on request for audit purposes.

        3.102 Where social housing is let at or above the market rate, the property can be treated as
        a private domestic premises, where the occupant meets the eligibility requirements.
        See section on PRS from paragraph 1.13 for more information.

        This method searches ALL of the possible measures that can be implemented under GBIS. This includes:
        - cavity wall (including party wall)
        - loft
        - solid wall
        - pitched roof
        - flat roof
        - under-floor
        - solid floor
        - park home
        - room-in-roof

        :return:
        """

        self.cavity_insulation()
        self.loft_insulation()
        self.solid_wall_insulation()
        self.room_roof_insulation()
        self.flat_roof_insulation()
        self.suspended_floor_insulation()
        self.solid_floor_insulation()

        current_sap = int(self.epc["current-energy-efficiency"])
        is_below_e = current_sap <= 54
        is_below_c = current_sap <= 68

        needs_measure = (
            self.cavity["suitability"] or
            self.loft["suitability"] or
            self.solid_wall["suitability"] or
            self.room_roof["suitability"] or
            self.flat_roof["suitability"] or
            self.suspended_floor["suitability"] or
            self.solid_floor["suitability"]
        )

        if self.tenure == "Rented (social)":

            if is_below_c and (not is_below_e):
                # this is a placeholder methodology
                self.gbis = {
                    "eligible": int(self.epc["potential-energy-efficiency"]) > 68,
                    "message": "contingent on innovation measure delivery"
                }
                return
            elif is_below_e:
                self.gbis = {
                    "eligible": needs_measure,
                    "message": "eligible under fabric measure"
                }
                return
            else:
                self.gbis = {
                    "eligible": False,
                    "message": "not eligible"
                }
                return

        elif self.tenure == "Rented (private)":
            self.gbis = {
                "eligible": is_below_c and needs_measure,
                "message": "eligible under fabric measure"
            }
            return
        elif self.tenure == "Owner-occupied":
            self.gbis = {
                "eligible": False,
                "message": "Out-of-scope"
            }
            return

        elif (self.tenure is None) or self.tenure == "unknown":
            self.gbis = {
                "eligible": needs_measure,
                "message": "unknown tenure"
            }
            return
        else:
            raise ValueError("Implement me other tenure types")

    def check_eco4(self):
        """
        Because ECO4 supports nearly all measures. If we have commercial agreements in place then a large number
        of homes would be eligible for eco funding, if identified.

        These are the eligibility criteria we consider for this process:
        Privately rented, Help to heat group
        - Sap E-G
        - Must receive one of solid wall insulation, first time central heating or district heating control
        - The property must already have cavity walls and roof insulated

        Social Housing, SAP D
        - Innovation measures and insulation measures to meet the minimum insulation requirement
        - Improvement to at least band C
        - Fabric measures
        - If receiving any heating measures, must have at least one insulation measure first

        Social Housing, SAP E-G
        - Insulation measures, first time central heating, renewable heating, district heating connection,
        innovation measures
        - Improvement to D (F & G properties) or C (E properties)
        - If receiving any heating measure, must already have cavity and roof insulation

        Privately rented, ECO4 Flex route 1, 2, 3, 4
        - Must have SAP E-G
        - Most measures eligible, but must receive one of solid wall insulation, first time central heating,
        renewable heating and district heating control
        - Improvement to D (F & G properties) or C (E properties)
        - All homes receiving heating measures must first have insulated cavity/roof


        The flex routes are given here:
        https://so-eco.co.uk/what-is-eco4-flex/#:~:text=One%20way%20to%20gain%20ECO4,
        including%20elderly%20residents%20and%20lodgers.

        :return:
        """

        self.cavity_insulation()
        self.loft_insulation()
        self.solid_wall_insulation()
        self.room_roof_insulation()
        self.flat_roof_insulation()
        self.suspended_floor_insulation()
        self.solid_floor_insulation()

        current_sap = int(self.epc["current-energy-efficiency"])
        is_below_e = current_sap <= 54
        is_below_c = current_sap <= 68
        sap_potential = int(self.epc["potential-energy-efficiency"])

        first_time_central_heating = "boiler" not in self.epc["mainheat-description"].lower()

        needs_fabric_measure = (
            self.cavity["suitability"] or
            self.loft["suitability"] or
            self.solid_wall["suitability"] or
            self.room_roof["suitability"] or
            self.flat_roof["suitability"] or
            self.suspended_floor["suitability"] or
            self.solid_floor["suitability"]
        )

        if current_sap <= 38 and sap_potential >= 55:
            # sap needs to get to at least a D
            expected_to_meet_upgrades = True
        elif current_sap <= 68 and sap_potential >= 69:
            # sap needs to get to at least a C
            expected_to_meet_upgrades = True
        else:
            expected_to_meet_upgrades = False

        if self.tenure == "Rented (social)":
            if is_below_c and (not is_below_e) and expected_to_meet_upgrades:
                # If the property is a D, then it's eligible under innovation measures but requires improvement to a
                # band C
                self.eco4 = {
                    "eligible": True,
                    "message": "eligible under innovation measure and improvement to band C"
                }
            elif is_below_e and expected_to_meet_upgrades:
                # If the property is an E or below, then it's eligible under fabric measures or heating/innovation
                # measures

                message = "eligible under fabric measures, with sufficient post retrofit sap improvement" if (
                    needs_fabric_measure) else (
                    "eligible under heating and innovation measures, with sufficient post retrofit sap improvement"
                )

                self.eco4 = {"eligible": True, "message": message}
            else:
                if (current_sap <= 68) and expected_to_meet_upgrades:
                    raise ValueError("something is wrong")
                self.eco4 = {
                    "eligible": False,
                    "message": "not eligible, above EPC C"
                }

            return

        if self.tenure == 'Rented (private)':
            # For private homes, the property needs to be an E or below

            # For private homes, the cavity must be filled and the roof insulated
            cavity_filled = not self.cavity["suitability"]
            roof_insulated = (not self.loft["suitability"]) and (not self.room_roof["suitability"]) and (
                not self.flat_roof["suitability"])

            if is_below_e and cavity_filled and roof_insulated and expected_to_meet_upgrades:

                if self.solid_wall["suitability"]:
                    self.eco4 = {
                        "eligible": True,
                        "message": "eligible under solid wall insulation, conditional on post retrofit sap and help "
                                   "to heat/ECO flex route"
                    }
                elif first_time_central_heating:

                    self.eco4 = {
                        "eligible": True,
                        "message": "eligible under first time central heating, conditional on post retrofit sap and "
                                   "help to heat/ECO flex route"
                    }
                else:
                    self.eco4 = {
                        "eligible": False,
                        "message": "not eligible at this time"
                    }

                return

            else:
                self.eco4 = {
                    "eligible": False,
                    "message": "not eligible at this time, EPC too high"
                }

        self.eco4 = {
            "eligible": False,
            "message": "Out of scope"
        }
