import numpy as np
from recommendations.county_to_region import county_to_region_map

# This data comes from SPONs 2023
regional_labour_variations = [
    {"Region": "Outer London", "Adjustment_Factor": 1.00},
    {"Region": "Inner London", "Adjustment_Factor": 1.05},
    {"Region": "South East England", "Adjustment_Factor": 0.96},
    {"Region": "South West England", "Adjustment_Factor": 0.90},
    {"Region": "East of England", "Adjustment_Factor": 0.93},
    {"Region": "East Midlands", "Adjustment_Factor": 0.88},
    {"Region": "West Midlands", "Adjustment_Factor": 0.87},
    {"Region": "North East England", "Adjustment_Factor": 0.83},
    {"Region": "North West England", "Adjustment_Factor": 0.88},
    {"Region": "Yorkshire and the Humber", "Adjustment_Factor": 0.86},
    {"Region": "Wales", "Adjustment_Factor": 0.88},
    {"Region": "Scotland", "Adjustment_Factor": 0.88},
    {"Region": "Northern Ireland", "Adjustment_Factor": 0.76}
]

# This data is based on the MCS database
MCS_SOLAR_PV_COST_DATA = {
    "last_updated": "2024-06-10",
    "average_cost_per_kwh": 1750,
    "average_cost_per_kwh-Outer London": 1776,
    "average_cost_per_kwh-Inner London": 1776,
    "average_cost_per_kwh-South East England": 1672,
    "average_cost_per_kwh-South West England": 1732,
    "average_cost_per_kwh-East of England": 1721,
    "average_cost_per_kwh-East Midlands": 1730,
    "average_cost_per_kwh-West Midlands": 1761,
    "average_cost_per_kwh-North East England": 1669,
    "average_cost_per_kwh-North West England": 1764,
    "average_cost_per_kwh-Yorkshire and the Humber": 1705,
    "average_cost_per_kwh-Wales": 1896,
    "average_cost_per_kwh-Scotland": 1767,
    "average_cost_per_kwh-Northern Ireland": 1767,
}

# This data is based on the MCS database, We use the larger figure between the 2023 and 2024 average,
# to be conservative
MCS_AIR_SOURCE_HEAT_PUMP_COST_DATA = {
    "Outer London": 13220,
    "Inner London": 13220,
    "South East England": 13547,
    "South West England": 12776,
    "East of England": 12585,
    "East Midlands": 12239,
    "West Midlands": 13182,
    "North East England": 11829,
    "North West England": 11714,
    "Yorkshire and the Humber": 11919,
    "Wales": 13701,
    "Scotland": 12586,
    "Northern Ireland": 12000,  # There are hardly any air source heat pump installs going on in Northern Ireland
}
BOILER_UPGRADE_SCHEME_ASHP_VALUE = 7500

# This is based on quotes from installers
BATTERY_COST = 3500

# This is based on https://www.checkatrade.com/blog/cost-guides/cost-smart-thermostat/
SMART_APPLIANCE_THERMOSTAT_COST = 400
PROGRAMMER_COST = 120
ROOM_THERMOSTAT_COST = 150
TRVS_COST = 35

# Cost for TTZC
# Smart thermostat based on checkatrade https://www.checkatrade.com/blog/cost-guides/cost-smart-thermostat/
# Based on the Nest system
TTZC_SMART_THERMOSTAT_COST = 205
TTZC_SMART_THERMOSTAT_LABOUR_HOURS = 2
TTZC_ELECTRICIAN_HOURLY_RATE = 45
# Based on cost of a Nest temperature sensor
TTZC_ROOM_TEMPERATURE_SENSOR_COST = 50
TTZC_ROOM_TEMPERATURE_SENSOR_LABOUR_HOURS = 0.17  # (Assume ~ 10 mins install per sensor)
# Basedon an average cost of smart radiator values
TTZC_SMART_RADIATOR_VALUES = 50
TTZC_SMART_RADIATOR_VALUES_LABOUR_HOURS = 0.37  # (Assume ~ 15-30 mins install per valve)

# Low carbon combi boiler - median value based on £2200 - £3000 range
LOW_CARBON_COMBI_BOILER = 2200

# boiler prices based on
# https://www.greenmatch.co.uk/boilers/30kw-boiler
# https://www.greenmatch.co.uk/boilers/35kw-boiler
# https://www.greenmatch.co.uk/boilers/40kw-boiler
# These are exclusive of installation costs
CONDENSING_BOILER_COSTS = {
    "30kw": 1550,
    "35kw": 1610,
    "40kw": 1625
}

# Assumes 3 hours to remove each heater (including re-decorating)
ROOM_HEATER_REMOVAL_COST = 120
ROOM_HEATER_REMOVAL_LABOUR_HOURS = 3

# This is a cost quoted by Jim for a system flush - existig system will run more efficiently
SYSTEM_FLUSH_COST = 250

SINGLE_RADIATOR_COST = 150
DOUBLE_RADIATOR_COST = 300
FLUE_COST = 600
PIPEWORK_COST = 750  # Min cost is £500

# Based on SCIS figures
# TODO: Add this to databse
CAVITY_EXTRACTION_COST = 25


class Costs:
    """
    A class to calculate the costs associated with construction works,
    specifically focusing on cavity wall insulation.
    It includes contingency, preliminaries, profit margin, and VAT calculations.

    As a sense check, there is a useful article from checkatrade on retrofitting and expected costs:
    https://www.checkatrade.com/blog/cost-guides/retrofit-insulation-cost/

    Another useful article for benchmarking the cost of floor insulation:
    https://www.checkatrade.com/blog/cost-guides/floor-insulation-cost/
    """

    # Contingency is a percentage of the total cost of the work and covers unforseen expenses
    # We assume a conservative 10% contingency for all works which is a rate defined by SPONs
    CONTINGENCY = 0.1

    # For flat roof, we assume it's a high risk project as it's very weather dependent and also is heavily
    # dependent on the quality of the existing roof
    FLAT_ROOF_CONTINGENCY = 0.15

    # We use a higher contingency rate for internal wall insulation because of the potential for issues with moving
    # fittings and trimming doors, as well as scope for damage to the existing wall during preparation.
    IWI_CONTINGENCY = 0.2

    # Where there is more uncertainty, a higher contingency rate is used
    HIGH_RISK_CONTINGENCY = 0.2
    # When there is less uncertainty, a lower contingency rate is used
    LOW_RISK_CONTINGENCY = 0.05

    # Preliminaries are a percentage of the total cost of the work and covers the cost of site-specific costs
    # such as site preparation, safety measures and project management. This rate can vary but we'll assume a 10%
    # rate, on the total cost before VAT, as recommended by SPONs
    PRELIMINARIES = 0.1

    # For higher risk projects, a higher preliminaries rate is used. SPONs indicates that a higher risk project might
    # have a preliminaries of 12-14% so we use 12% as the median for the preliminaries rate.
    # For External wall insulation (EWI), we use 15% as the preliminaries rate if we think the property might
    # need scaffolding, otherwise we use 12%. This is to account for any site preparation that might be required
    EWI_NO_SCAFFOLDING_PRELIMINARIES = 0.2
    EWI_SCAFFOLDING_PRELIMINARIES = 0.25

    VAT_RATE = 0.2
    PROFIT_MARGIN = 0.2

    # Based on this greenmatch article, on average, a Sash window is around 50% more expensive than a casement window.
    # Therefore, for a conservative cost estimate, and allowance for a more premium window type, we inflate the material
    # cost of the windows to allow for a sash window type
    # https://www.greenmatch.co.uk/windows/double-glazing/cost
    SASH_WINDOW_INFLATION_FACTOR = 1.5

    # Typically, secondary glazing can be installed for 25% of the cost of double glazed windows - to be conservative,
    # we scale the cost by half
    SECONDARY_GLAZING_SCALING_FACTOR = 0.5

    def __init__(self, property_instance):
        """
        Initializes the Costs class with a property instance.

        :param property_instance: Instance of a Property class containing relevant details like wall area.
        """
        if not hasattr(property_instance, 'insulation_wall_area'):
            raise ValueError("Property instance must have an 'insulation_wall_area' attribute")
        self.property = property_instance
        self.regional_labour_variations = regional_labour_variations

        self.region = county_to_region_map.get(self.property.data["county"], None)
        if self.region is None:
            # Try and grab using the local-authority-label
            self.region = county_to_region_map.get(self.property.data["local-authority-label"], None)
            if self.region is None:
                raise ValueError("Region not found in county map")

        self.labour_adjustment_factor = [
            x["Adjustment_Factor"] for x in self.regional_labour_variations if
            x["Region"] == self.region
        ][0]

        if not self.labour_adjustment_factor:
            raise ValueError("Labour adjustment factor not found")

    def cavity_wall_insulation(self, wall_area, material, is_extraction_and_refill=False):
        """
        Calculates the total cost for cavity wall insulation based on material and labor costs,
        including contingency, preliminaries, profit, and VAT.

        Because of some limitations in the SPONs data, there are no materials that can be blown through a wall,
        therefore we have adapted similar materials, basing our estimates on 75mm cavity slabs, and have halved the
        labour time required. That is why we still price based on wall area despite volume actually being the correct
        metric.

        :return: A dictionary containing detailed cost breakdown.
        """

        # CWI usually takes 1 day
        labour_hours = 8
        labour_days = 1

        # if the material is based on an installer cost, we return the flat price
        if material["is_installer_quote"]:
            total_cost = material["total_cost"] * wall_area

            return {
                "total": total_cost,
                "labour_hours": labour_hours,
                "labour_days": labour_days,
            }

        material_cost_per_m2 = material["material_cost"]

        base_material_cost = material_cost_per_m2 * wall_area
        labour_cost = material["labour_cost"] * wall_area * self.labour_adjustment_factor

        subtotal_before_profit = base_material_cost + labour_cost

        contingency_cost = subtotal_before_profit * self.CONTINGENCY
        preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
        profit_cost = subtotal_before_profit * self.PROFIT_MARGIN

        subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost

        vat_cost = subtotal_before_vat * self.VAT_RATE

        total_cost = subtotal_before_vat + vat_cost

        if is_extraction_and_refill:
            # bump up the cost of the work
            total_cost = total_cost + CAVITY_EXTRACTION_COST * wall_area
            # Additional 2 days work
            labour_hours = labour_hours + (2 * 8)
            labour_days = labour_days + 2

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat_cost,
            "contingency": contingency_cost,
            "preliminaries": preliminaries_cost,
            "material": base_material_cost,
            "profit": profit_cost,
            "labour_hours": labour_hours,
            "labour_cost": labour_cost,
            "labour_days": labour_days
        }

    def loft_insulation(self, floor_area, material):
        """
        Calculates the total cost for cavity wall insulation based on material and labor costs,
        including contingency, preliminaries, profit, and VAT.

        :return: A dictionary containing detailed cost breakdown.
        """

        labour_hours = material["labour_hours_per_unit"] * floor_area
        # Assume a team of 1 person
        labour_days = labour_hours / 8

        if material["is_installer_quote"]:
            total_cost = material["total_cost"] * floor_area

            return {
                "total": total_cost,
                "labour_hours": labour_hours,
                "labour_days": labour_days,
            }

        material_cost_per_m2 = material["material_cost"]

        # We inflate material costs due to recent price increases
        material_cost_per_m2 = material_cost_per_m2 * 1.5

        base_material_cost = material_cost_per_m2 * floor_area
        labour_cost = material["labour_cost"] * floor_area * self.labour_adjustment_factor

        subtotal_before_profit = base_material_cost + labour_cost

        # We use high risk contingency because of the possibility of access issues and clearing existing insulation
        contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY
        preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
        profit_cost = subtotal_before_profit * self.PROFIT_MARGIN

        subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost

        vat_cost = subtotal_before_vat * self.VAT_RATE

        total_cost = subtotal_before_vat + vat_cost

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat_cost,
            "contingency": contingency_cost,
            "preliminaries": preliminaries_cost,
            "material": base_material_cost,
            "profit": profit_cost,
            "labour_hours": labour_hours,
            "labour_cost": labour_cost,
            "labour_days": labour_days
        }

    def internal_wall_insulation(self, wall_area, material, non_insulation_materials):
        """
        Broadly speaking, the high level steps to an internal wall insulation job are the following:

        1) Demolition: This involves removing existing wall linings, fittings, and any other obstacles.
        It's important to factor in the disposal of debris and the potential need for additional protective
        measures to ensure the safety of the work area.

        2) Insulation Installation: This is the core part of the process where the chosen insulation material is
        applied. The choice of insulation material will depend on several factors including thermal performance,
        wall construction, and space constraints.

        3) Vapour Barrier Installation: This is crucial for preventing moisture from penetrating the insulation,
        which can compromise its effectiveness and lead to mold growth.

        4) Re-decoration: This involves applying plaster to the wall and then painting.
        The quality of finish here is important for both aesthetic and functional reasons.

        5) Trim and Finishing Work: Post-insulation, tasks such as re-installing skirting boards, door frames,
        or window sills might be necessary.
        :return:
        """

        # if the material is based on an installer cost, we return the flat price
        if material["is_installer_quote"]:
            total_cost = material["total_cost"] * wall_area

            labour_hours = material["labour_hours_per_unit"] * wall_area

            # To install internal wall insulation, a small to medium size project might be conducted by a team of 3-5
            # people
            labour_days = (labour_hours / 8) / 4

            return {
                "total": total_cost,
                "labour_hours": labour_hours,
                "labour_days": labour_days,
            }

        # Extract and check the different types of data we'll need
        demolition_data = [x for x in non_insulation_materials if x["type"] == "iwi_wall_demolition"]
        vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "iwi_vapour_barrier"]
        redecoration_data = [x for x in non_insulation_materials if x["type"] == "iwi_redecoration"]
        if not demolition_data:
            raise ValueError("No data found for iwi_wall_demolition")

        if (len(vapour_barrier_data) != 1) or (len(redecoration_data) != 3):
            raise ValueError("Incorrect number of data entries for non-insulation materials")

        # Break out the individual material costs
        # Since we don't know the exact wall construction, we take an average for demolition costs, since
        # the cost will depend on the type of wall construction
        demolition_material_costs = np.mean([x["material_cost"] * wall_area for x in demolition_data])
        insulation_material_costs = material["material_cost"] * wall_area
        vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * wall_area
        redecoration_material_costs = sum([x["material_cost"] * wall_area for x in redecoration_data])

        demolition_plant_costs = np.mean([x["plant_cost"] * wall_area for x in demolition_data])

        # Again for demolition, we average since we aren't sure which demolition process will be used
        demolition_labour_costs = np.mean([x["labour_cost"] * wall_area for x in demolition_data])
        insulation_labour_costs = material["labour_cost"] * wall_area
        vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * wall_area
        redecoration_labour_costs = sum([x["labour_cost"] * wall_area for x in redecoration_data])

        labour_costs = (demolition_labour_costs + insulation_labour_costs + vapour_barrier_labour_costs +
                        redecoration_labour_costs)

        labour_costs = labour_costs * self.labour_adjustment_factor

        materials_costs = (demolition_material_costs + insulation_material_costs + vapour_barrier_material_costs +
                           redecoration_material_costs)

        subtotal_before_profit = labour_costs + materials_costs + demolition_plant_costs

        contingency_cost = subtotal_before_profit * self.IWI_CONTINGENCY
        preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
        profit_cost = subtotal_before_profit * self.PROFIT_MARGIN

        subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost

        vat_cost = subtotal_before_vat * self.VAT_RATE

        total_cost = subtotal_before_vat + vat_cost

        demolition_labour_hours = np.mean([x["labour_hours_per_unit"] * wall_area for x in demolition_data])
        insulation_labour_hours = material["labour_hours_per_unit"] * wall_area
        vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * wall_area
        redecoration_labour_hours = sum([x["labour_hours_per_unit"] * wall_area for x in redecoration_data])

        labour_hours = (demolition_labour_hours + insulation_labour_hours + vapour_barrier_labour_hours +
                        redecoration_labour_hours)

        # To install internal wall insulation, a small to medium size project might be conducted by a team of 3-5 people
        labour_days = (labour_hours / 8) / 4

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat_cost,
            "contingency": contingency_cost,
            "preliminaries": preliminaries_cost,
            "material": materials_costs,
            "profit": profit_cost,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
            "labour_cost": labour_costs
        }

    def suspended_floor_insulation(self, insulation_floor_area, material, non_insulation_materials):
        """
        We characterise the steps for suspended floor insulation as the following tasks:

        1) Removal of Carpet and Underfelt: Where necessary, remove existing floor coverings to access the floorboards.
        2) Removal of Floor Boarding: Carefully remove floorboards to access the space beneath for insulation.
        3) Installation of Vapour Barrier: Install a vapour barrier to prevent moisture from affecting
        the insulation and floor structure.
        4) Installation of Insulation: Fit the chosen insulation material between the joists in the floor void.
        5) Refixing Floorboards: Replace and secure the floorboards after insulation installation.
        6) Re-carpeting: Lay down the carpet or other floor coverings once the insulation and floorboards are in place.
        :return:
        """

        # if the material is based on an installer cost, we return the flat price
        if material["is_installer_quote"]:
            total_cost = material["total_cost"] * insulation_floor_area

            labour_hours = material["labour_hours_per_unit"] * insulation_floor_area
            # To install suspended floor insulation, a small to medium size project might be conducted by a team of 3
            # people
            labour_days = (labour_hours / 8) / 3

            return {
                "total": total_cost,
                "labour_hours": labour_hours,
                "labour_days": labour_days,
            }

        demolition_data = [x for x in non_insulation_materials if x["type"] == "suspended_floor_demolition"]
        vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "suspended_floor_vapour_barrier"]
        redecoration_data = [x for x in non_insulation_materials if x["type"] == "suspended_floor_redecoration"]

        if (len(demolition_data) != 2) or (len(vapour_barrier_data) != 1) or (len(redecoration_data) != 2):
            raise ValueError("Incorrect number of data entries for non-insulation materials")

        # Break out the individual material costs
        demolition_material_costs = sum([x["material_cost"] * insulation_floor_area for x in demolition_data])
        insulation_material_costs = material["material_cost"] * insulation_floor_area
        vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * insulation_floor_area
        redecoration_material_costs = sum([x["material_cost"] * insulation_floor_area for x in redecoration_data])

        demolition_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in demolition_data])
        insulation_labour_costs = material["labour_cost"] * insulation_floor_area
        vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * insulation_floor_area
        redecoration_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in redecoration_data])

        labour_costs = (demolition_labour_costs + insulation_labour_costs + vapour_barrier_labour_costs +
                        redecoration_labour_costs)

        labour_costs = labour_costs * self.labour_adjustment_factor

        materials_costs = (demolition_material_costs + insulation_material_costs + vapour_barrier_material_costs +
                           redecoration_material_costs)

        subtotal_before_profit = labour_costs + materials_costs

        # Because of the possiblity of damage to the existing floor, or difficulties associated to moving fittings,
        # we use a higher contingency rate
        contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY
        preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
        profit_cost = subtotal_before_profit * self.PROFIT_MARGIN

        subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost

        vat_cost = subtotal_before_vat * self.VAT_RATE

        total_cost = subtotal_before_vat + vat_cost

        demolition_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in demolition_data])
        insulation_labour_hours = material["labour_hours_per_unit"] * insulation_floor_area
        vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * insulation_floor_area
        redecoration_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in redecoration_data])

        labour_hours = (demolition_labour_hours + insulation_labour_hours + vapour_barrier_labour_hours +
                        redecoration_labour_hours)

        # Assume a team of 3 people for a small to medium size project
        labour_days = (labour_hours / 8) / 3

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat_cost,
            "contingency": contingency_cost,
            "preliminaries": preliminaries_cost,
            "material": materials_costs,
            "profit": profit_cost,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
            "labour_cost": labour_costs
        }

    def solid_floor_insulation(self, insulation_floor_area, material, non_insulation_materials):
        """
        We characterise the steps for solid floor insulation as the following tasks:

        1) Removal of Carpet and Underfelt: This is the initial stage where any existing floor coverings, like carpets,
        tiles, or linoleum, are carefully removed. This exposes the solid floor beneath, which is typically concrete.

        2) Preparation of Flooring: This step is critical. It involves:
        - Cleaning the existing floor surface thoroughly to remove debris and ensure a flat surface.
        - Assessing and repairing any damage to the concrete floor. This might include filling cracks or leveling
        uneven areas.

        3) Installation of a Damp Proof Membrane (DPM): Before installing insulation, a DPM is often laid down to
        prevent moisture from rising into the insulation and the interior space. This step is crucial in areas prone to
        dampness.

        4) Install Insulation: The insulation, often in the form of rigid foam boards, is laid over the DPM.
        The choice of insulation material will depend on the desired thermal properties and the available floor height.
        Care is taken to minimize thermal bridges and ensure a snug fit between insulation boards.

        5) Laying a New Subfloor: Over the insulation, a new subfloor is often installed. This could be a layer of
        screed (a type of concrete) or wooden boarding, depending on the specific requirements and preferences.

        6) Re-decoration and Finishing Touches: Once the subfloor is in place and has set or dried (if necessary),
        the final floor finish can be applied. This might involve:
        - Laying new tiles, wooden flooring, or other chosen materials.
        - If you're planning to re-carpet, this would be the stage to do it.
        - Skirting boards may need to be refitted or replaced.

        7) Considerations for Doors and Fixtures: It's important to note that raising the floor level can affect door
        thresholds and other fixtures. Doors may need to be trimmed, and fixtures might need adjustments.

        :param insulation_floor_area: Area of the floor to be insulated
        :param material: Selected insulation material
        :param non_insulation_materials: Non-insulation materials required for the job
        :return:
        """

        # if the material is based on an installer cost, we return the flat price
        if material["is_installer_quote"]:
            total_cost = material["total_cost"] * insulation_floor_area

            labour_hours = material["labour_hours_per_unit"] * insulation_floor_area
            # To install suspended floor insulation, a small to medium size project might be conducted by a team of 3
            # people
            labour_days = (labour_hours / 8) / 3

            return {
                "total": total_cost,
                "labour_hours": labour_hours,
                "labour_days": labour_days,
            }

        demolition_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_demolition"]
        preparation_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_preparation"]
        vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_vapour_barrier"]
        redecoration_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_redecoration"]

        if ((len(demolition_data) != 1) or (len(preparation_data) != 2) or (len(vapour_barrier_data) != 1) or
            (len(redecoration_data) != 3)):
            raise ValueError("Incorrect number of data entries for non-insulation materials")

        # Break out the individual material costs
        preparation_material_costs = sum([x["material_cost"] * insulation_floor_area for x in preparation_data])
        insulation_material_costs = material["material_cost"] * insulation_floor_area
        vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * insulation_floor_area
        redecoration_material_costs = sum([x["material_cost"] * insulation_floor_area for x in redecoration_data])

        demolition_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in demolition_data])
        preparation_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in preparation_data])
        insulation_labour_costs = material["labour_cost"] * insulation_floor_area
        vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * insulation_floor_area
        redecoration_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in redecoration_data])

        preparation_plant_costs = sum([x["plant_cost"] * insulation_floor_area for x in preparation_data])

        labour_costs = (demolition_labour_costs + insulation_labour_costs + vapour_barrier_labour_costs +
                        redecoration_labour_costs + preparation_labour_costs)

        labour_costs = labour_costs * self.labour_adjustment_factor

        materials_cost = (preparation_material_costs + insulation_material_costs + vapour_barrier_material_costs +
                          redecoration_material_costs)

        subtotal_before_profit = labour_costs + materials_cost + preparation_plant_costs

        # We use HIGH_RISH_CONTINGENCY because of the potential for issues with moving fittings and trimming doors,
        # as well as scope for damage to the existing floor during preparation.
        contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY
        preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
        profit_cost = subtotal_before_profit * self.PROFIT_MARGIN

        subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
        vat_cost = subtotal_before_vat * self.VAT_RATE
        total_cost = subtotal_before_vat + vat_cost

        demolition_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in demolition_data])
        preparation_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in preparation_data])
        insulation_labour_hours = material["labour_hours_per_unit"] * insulation_floor_area
        vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * insulation_floor_area
        redecoration_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in redecoration_data])

        labour_hours = (demolition_labour_hours + insulation_labour_hours + vapour_barrier_labour_hours +
                        redecoration_labour_hours + preparation_labour_hours)

        # Assume a team of 3 people for a small to medium size project
        labour_days = (labour_hours / 8) / 3

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat_cost,
            "contingency": contingency_cost,
            "preliminaries": preliminaries_cost,
            "material": materials_cost,
            "profit": profit_cost,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
            "labour_cost": labour_costs
        }

    def external_wall_insulation(self, wall_area, material, non_insulation_materials):
        """
        We characterise external wall insulation as the following steps:

        1) Preparation of the Area: Tidying up the surroundings, trimming back foliage, and laying down protective
        sheets to protect the flooring and landscaping around the work area.

        2) Scaffolding Setup (if needed): Erecting scaffolding for safe access to the walls of semi-detached or
        detached houses. For terraced houses or lower-level work, scaffolding might not be necessary.

        3) Wall Surface Preparation: Cleaning the wall surface, removing any loose or flaking material,
        and possibly applying a primer. If the existing wall is weak or damaged, partial or full replacement
        of the top surface may be necessary.

        4) Applying Primer: If the existing wall is suitable, applying a primer to improve adhesion of the insulation
        boards and stabilize the wall surface, especially if it's old or weathered.

        5) Insulation Application: Attaching insulation boards to the primed wall using adhesive, mechanical fixings,
        or a combination of both.

        6) Basecoat and Mesh Application: Applying a basecoat embedded with a reinforcing mesh over the insulation.
        This layer provides strength and helps prevent cracking.

        7) Decorative Finish: Applying a decorative finish, such as render or cladding, which protects the insulation
        and provides an aesthetic look.

        8) Reinstalling Fixtures: Reattaching any fixtures like downpipes, satellite dishes, or lighting fixtures that
        were removed during preparation. Extensions or adjustments may be required due to the increased wall thickness.

        9) Inspection and Cleanup: Conducting a thorough inspection to ensure quality and integrity of the EWI system,
        followed by cleaning up the site to remove all debris and materials.

        In the actual materials data, at this point, we have costing for:
        - wall preparation, hacking off existing wall finishes, linings, etc (ewi_wall_demolition)
        - wall surface cleaning and priming (ewi_wall_preparation)
        - insulation (external_wall_insulation)
        - basecoat and mesh with decorative render topcoat finish (ewi_basecoat_and_mesh)

        All of this data comes from SPONS, however there are some clear features missing. Because we could not find
        suitable cost records in SPONS for steps like cleaning the area, setting up small scale scaffolding,
        re-attaching any fitings and cleaning up the area afterwards, instead we have accounted for these steps by
        increasing the preliminaries rate. It is acknowldeged though, that this is not ideal and that the cost of these
        steps should be included in the materials data. We will look to improve this in the future, with data from
        installers

        :param wall_area:
        :param material:
        :param non_insulation_materials:
        :return:
        """

        if material["is_installer_quote"]:
            total_cost = material["total_cost"] * wall_area
            # Add on a buffer for scaffolding
            if self.property.data["property-type"] == "House":
                total_cost += self.EWI_SCAFFOLDING_PRELIMINARIES * total_cost

            labour_hours = material["labour_hours_per_unit"] * wall_area

            # To install internal wall insulation, a small to medium size project might be conducted by a team of 3-5
            # people
            labour_days = (labour_hours / 8) / 4

            return {
                "total": total_cost,
                "labour_hours": labour_hours,
                "labour_days": labour_days,
            }

        # For semi detatched and detatched houses, as well as maisonettes, we price for scaffolding

        if self.property.data["property-type"] == "House":
            if self.property.data["built-form"] in ['Semi-Detached', 'Detached', "End-Terrace"]:
                preliminaries_rate = self.EWI_SCAFFOLDING_PRELIMINARIES
            else:
                preliminaries_rate = self.EWI_NO_SCAFFOLDING_PRELIMINARIES
        elif self.property.data["property-type"] in ["Maisonette", "Flat"]:
            preliminaries_rate = self.EWI_SCAFFOLDING_PRELIMINARIES
        elif self.property.data["property-type"] == "Bungalow":
            preliminaries_rate = self.EWI_NO_SCAFFOLDING_PRELIMINARIES

        demolition_data = [x for x in non_insulation_materials if x["type"] == "ewi_wall_demolition"]
        preparation_data = [x for x in non_insulation_materials if x["type"] == "ewi_wall_preparation"]
        redecoration_data = [x for x in non_insulation_materials if x["type"] == "ewi_wall_redecoration"]

        if (len(demolition_data) != 3) or (len(preparation_data) != 1) or (len(redecoration_data) != 1):
            raise ValueError("Incorrect number of data entries for non-insulation materials")

        # Break out the individual material costs
        # Since we don't know the exact wall construction, we take an average for demolition costs, since
        # the cost will depend on the type of wall construction
        demolition_material_costs = np.mean([x["material_cost"] * wall_area for x in demolition_data])
        insulation_material_costs = material["material_cost"] * wall_area
        preparation_material_costs = preparation_data[0]["material_cost"] * wall_area
        redecoration_material_costs = redecoration_data[0]["material_cost"] * wall_area

        demolition_plant_costs = np.mean([x["plant_cost"] * wall_area for x in demolition_data])

        demolition_labour_costs = np.mean([x["labour_cost"] * wall_area for x in demolition_data])
        insulation_labour_costs = material["labour_cost"] * wall_area
        preparation_labour_costs = preparation_data[0]["labour_cost"] * wall_area
        redecoration_labour_costs = redecoration_data[0]["labour_cost"] * wall_area

        labour_costs = (demolition_labour_costs + insulation_labour_costs + redecoration_labour_costs +
                        preparation_labour_costs)

        labour_costs = labour_costs * self.labour_adjustment_factor

        materials_costs = (demolition_material_costs + insulation_material_costs + preparation_material_costs +
                           redecoration_material_costs)

        subtotal_before_profit = labour_costs + materials_costs + demolition_plant_costs

        contingency_cost = subtotal_before_profit * self.CONTINGENCY
        preliminaries_cost = subtotal_before_profit * preliminaries_rate
        profit_cost = subtotal_before_profit * self.PROFIT_MARGIN

        subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
        vat_cost = subtotal_before_vat * self.VAT_RATE
        total_cost = subtotal_before_vat + vat_cost

        demolition_labour_hours = np.mean([x["labour_hours_per_unit"] * wall_area for x in demolition_data])
        insulation_labour_hours = material["labour_hours_per_unit"] * wall_area
        preparation_labour_hours = preparation_data[0]["labour_hours_per_unit"] * wall_area
        redecoration_labour_hours = redecoration_data[0]["labour_hours_per_unit"] * wall_area

        labour_hours = (demolition_labour_hours + insulation_labour_hours + redecoration_labour_hours +
                        preparation_labour_hours)

        # Assume a team of 3-5 people for a small to medium size project
        labour_days = (labour_hours / 8) / 4

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat_cost,
            "contingency": contingency_cost,
            "preliminaries": preliminaries_cost,
            "material": materials_costs,
            "profit": profit_cost,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
            "labour_cost": labour_costs
        }

    def low_energy_lighting(self, number_of_lights, number_current_lel_lights, material):

        """
        Calculates the total cost for low energy lighting based on material and labor costs,
        including contingency, preliminaries, profit, and VAT.

        :param number_of_lights: Int, number of light
        :param number_current_lel_lights:  Int, number of low energy lights currently installed in the home
        :material: Dict, material data containing costs of fittings
        """

        # If there are no lights fitted in the property, we increase the contingency in case there are potential wiring
        # blockers
        if number_current_lel_lights == 0:
            contingency = self.HIGH_RISK_CONTINGENCY
        else:
            contingency = self.CONTINGENCY

        material_cost = material["material_cost"] * number_of_lights
        labour_cost = material["labour_cost"] * number_of_lights * self.labour_adjustment_factor

        subtotal_before_profit = material_cost + labour_cost

        contingency_cost = subtotal_before_profit * contingency
        preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
        profit_cost = subtotal_before_profit * self.PROFIT_MARGIN

        subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
        vat_cost = subtotal_before_vat * self.VAT_RATE
        total_cost = subtotal_before_vat + vat_cost

        labour_hours = material["labour_hours_per_unit"] * number_of_lights
        # Assume a single electrician installing
        labour_days = (labour_hours / 8)

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat_cost,
            "contingency": contingency_cost,
            "preliminaries": preliminaries_cost,
            "material": material_cost,
            "profit": profit_cost,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
            "labour_cost": labour_cost
        }

    def flat_roof_insulation(self, floor_area, material, non_insulation_materials):
        """
        A model of a warm, flat roof construction can be seen in this video:
        https://www.youtube.com/watch?v=WZ6Ng6YI9OA
        Warm, flat roof insulation will normally be 100-125mm in depth

        We break this measure down into the following jobs to be done
        1) Preparation of the room. This involves cleaning the existing roof surface, removing any debris and repairing
        any damage. Additionally, an edge barrier will likely need to be installed, to protect the sides of the
        roof from water ingress.
        2) Primer Application. A layer of primer is applied to the clean roof surface to enhance the adhestia of
        subsequent layers, and seal the existing roof surface.
        3) Vapour Proof Layer Installation. Lay a vapour control layer to prevent moisture ingress from inside the
        building, which is essential in warm roof construction.
        4) Insulation Layer Application. Place and securely fix insulation boards over the roof. These could be rigid
        boards like PIR (Polyisocyanurate).
        5) Waterproofing Membrane Installation: Cover the insulation (and timber layer, if used) with a
        waterproofing membrane, like EPDM, PVC, or bituminous felt.  Carefully seal all joints, edges, and around any
        roof penetrations to ensure water tightness

        :param floor_area: Area of the flat roof to be insulated, based on the area of the floor
        :param material: Selected insulation material
        :param non_insulation_materials: Non-insulation materials required for the job
        :return:
        """

        preparation_data_m2 = [
            x for x in non_insulation_materials if
            (x["type"] == "flat_roof_preparation") and (x["cost_unit"] == "gbp_per_m2")
        ]
        vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "flat_roof_vapour_barrier"]
        waterproofing_data = [x for x in non_insulation_materials if x["type"] == "flat_roof_waterproofing"]

        if (len(preparation_data_m2) != 2) or (len(vapour_barrier_data) != 1) or (
            len(waterproofing_data) != 1):
            raise ValueError("Incorrect number of data entries for non-insulation materials")

        # Break out the individual material costs
        preparation_m2_material_costs = sum([x["material_cost"] * floor_area for x in preparation_data_m2])
        vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * floor_area
        insulation_material_costs = material["material_cost"] * floor_area

        preparation_m2_labour_costs = sum([x["labour_cost"] * floor_area for x in preparation_data_m2])
        vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * floor_area

        # For waterproofing and upstand, we only have a total cost
        waterproofing_total_costs = waterproofing_data[0]["total_cost"] * floor_area

        labour_costs = preparation_m2_labour_costs + vapour_barrier_labour_costs
        labour_costs = labour_costs * self.labour_adjustment_factor

        materials_costs = preparation_m2_material_costs + vapour_barrier_material_costs + insulation_material_costs

        subtotal_before_profit = labour_costs + materials_costs + waterproofing_total_costs

        contingency_cost = subtotal_before_profit * self.FLAT_ROOF_CONTINGENCY
        preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
        profit_cost = subtotal_before_profit * self.PROFIT_MARGIN

        subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
        vat_cost = subtotal_before_vat * self.VAT_RATE
        total_cost = subtotal_before_vat + vat_cost

        preparation_m2_labour_hours = sum([x["labour_hours_per_unit"] * floor_area for x in preparation_data_m2])
        vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * floor_area
        waterproofing_labour_hours = waterproofing_data[0]["labour_hours_per_unit"] * floor_area

        labour_hours = preparation_m2_labour_hours + vapour_barrier_labour_hours + waterproofing_labour_hours

        # To install flat roof insulation, assume a small/medium project might be conducted by a team of 2-4.
        # We'll assume a team of 2 since a lot of the roofs will be on the smaller side and will review this later
        labour_days = (labour_hours / 8) / 2

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat_cost,
            "contingency": contingency_cost,
            "preliminaries": preliminaries_cost,
            "material": materials_costs,
            "profit": profit_cost,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
            "labour_cost": labour_costs
        }

    def window_glazing(self, number_of_windows, material, is_secondary_glazing=False):
        """
        We characterise the jobs to be done for window glazing as the following:
        1) Initial Assessment and Measurements: Before removing the existing window, it's essential to assess the
        condition of the window frame and opening. Precise measurements are taken to ensure the new double glazed
        windows fit perfectly.

        2) Remove the Existing Window: This involves carefully dismantling and removing the old single glazed window. It
        requires skill to avoid damaging the surrounding wall and the window frame (if it's to be reused).

        3) Dispose of the Existing Window: The old window, especially if it's a single glazed unit, needs to be
        disposed of responsibly. Glass and other materials should be recycled where possible.

        4) Surface Preparation: The window opening might need some preparation, especially if there's damage or if
        adjustments are needed to accommodate the new window. This can include repairing or replacing parts of the
        window frame, sealing gaps, and ensuring the opening is level and square.

        5) Install the Window Frame (if new frames are used): In many cases, double glazed windows come with their
        frames. These need to be installed securely into the window opening. This process involves aligning, leveling,
        and fixing the frame in place.

        6) Install the Window Sill: If a new window sill is required, it is installed at this stage. It needs to be
        correctly aligned with the frame and securely attached.

        7) Install the Double Glazed Glass Units: The glass units are carefully inserted into the frame. This step
        requires precision to ensure a snug fit without causing stress on the glass, which could lead to cracking or
        breaking.

        8) Sealing and Weatherproofing: After the glass units are in place, it's crucial to seal around the frame and
        between the glass and frame to ensure there are no drafts and that the installation is weather-tight. This
        typically involves applying silicone sealant or other appropriate sealing materials.

        9) Finishing Touches: This includes any cosmetic work, such as trimming, painting, or staining the frame and
        sill to match the rest of the property. It might also involve cleaning up any mess created during the
        installation.

        10) Inspection and Testing: Finally, the new windows should be inspected to ensure they open, close, and lock
        correctly. This is also a good time to check for any gaps or issues with the sealing.

        For this cost estimation process, we factor in initial assement into the preliminaries

        """

        if material["is_installer_quote"]:
            total_cost = material["total_cost"] * number_of_windows

            labour_hours = material["labour_hours_per_unit"] * number_of_windows
            # To install windows, a small to medium size project might be conducted by a team of 2-3 people
            labour_days = (labour_hours / 8) / 2

            return {
                "total": total_cost,
                "labour_hours": labour_hours,
                "labour_days": labour_days,
            }

        material_cost = material["material_cost"] * number_of_windows

        labour_cost = (
            material["labour_cost"] * number_of_windows * self.labour_adjustment_factor
        )
        multiplier = self.SECONDARY_GLAZING_SCALING_FACTOR if is_secondary_glazing else (
            self.SASH_WINDOW_INFLATION_FACTOR)

        subtotal = (material_cost + labour_cost) * multiplier

        contingency_cost = subtotal * self.CONTINGENCY
        preliminaries_cost = subtotal * self.PRELIMINARIES
        profit_cost = subtotal * self.PROFIT_MARGIN

        subtotal_before_vat = subtotal + contingency_cost + preliminaries_cost + profit_cost

        vat_cost = subtotal_before_vat * self.VAT_RATE

        total_cost = subtotal_before_vat + vat_cost

        labour_hours = material["labour_hours_per_unit"] * number_of_windows
        labour_hours = labour_hours * self.SECONDARY_GLAZING_SCALING_FACTOR if is_secondary_glazing else labour_hours

        # Assume a team of 2
        labour_days = (labour_hours / 8) / 2

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat_cost,
            "contingency": contingency_cost,
            "preliminaries": preliminaries_cost,
            "material": material_cost,
            "profit": profit_cost,
            "labour_hours": labour_hours,
            "labour_cost": labour_cost,
            "labour_days": labour_days
        }

    def solar_pv(self, wattage: float, has_battery: bool = False):

        """
        Calculates the total cost for solar PV based data provided by the MCS dashboard, which contains
        costing data for installations of renewable and clean energy measures.

        The data in the dashboard is filtered on domestic building installations and then the data across the
        various regions is manually collected. There is currently no automated way to get the data from the MCS
        dashboard

        Price can also be benchmarked against this checkatrade article:
        https://www.checkatrade.com/blog/cost-guides/cost-of-solar-panel-installation/
        :param wattage: Peak wattage of the solar PV system]
        :param has_battery: Bool, whether the system includes a battery
        """

        # Get the cost data relevant to the region
        regional_cost = MCS_SOLAR_PV_COST_DATA["-".join(["average_cost_per_kwh", self.region])]

        kw = wattage / 1000
        total_cost = kw * regional_cost

        if has_battery:
            # The battery cost is based on the £3500 quote, recieved from installers
            total_cost += BATTERY_COST

        subtotal_before_vat = total_cost / (1 + self.VAT_RATE)

        vat = total_cost - subtotal_before_vat

        # Labour hours are based on estimates from online research but an average team seems to consist of 3 people
        # and most jobs take around 2 days. Assuming an 8 hour day for 3 people across 2 days, gives us 72 hours of
        # labour
        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": 72,
            "labour_days": 2,
        }

    def programmer_and_appliance_thermostat(self, has_programmer):
        """
        Calculate the total cost of installing a programmer and appliance thermostat
        If the property already has a programmer, then the only thing we need to calculate the cost for is the
        appliance thermostat
        """

        if has_programmer:
            labour_hours = 2
            total_cost = SMART_APPLIANCE_THERMOSTAT_COST
        else:
            labour_hours = 4
            total_cost = SMART_APPLIANCE_THERMOSTAT_COST + PROGRAMMER_COST

        subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
        vat = total_cost - subtotal_before_vat

        # We estimate the cost of an appliance thermostat at £400, which is the upper end of the range
        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": labour_hours,
            "labour_days": 1,
        }

    def electric_room_heaters(self, number_heated_rooms):
        """
        We base the estimates for the cost of electric room heaters on the cost per room as estimated by the
        following article:
        https://www.bestelectricradiators.co.uk/blog/cost-to-install-a-new-heating-system-uk/
        
        :param number_heated_rooms: int, number of rooms to be heated
        :return: 
        """

        total_cost = 500 * number_heated_rooms
        subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
        vat = total_cost - subtotal_before_vat

        # TODO: Rough estimate to be reviewed
        labour_hours = 1 * number_heated_rooms
        labour_days = np.ceil(labour_hours / 8)

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }

    def high_heat_electric_storage_heaters(self, number_heated_rooms):

        """
        We base the estimates for the cost of electric storage heaters on the cost per room as estimated by the
        energy saving trust
        https://energysavingtrust.org.uk/advice/electric-heating/

        The cost is based on the number of heated rooms
        :param number_heated_rooms: int, number of rooms to be heated
        """

        total_cost = 1500 * number_heated_rooms
        subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
        vat = total_cost - subtotal_before_vat

        # TODO: Rough estimate to be reviewed
        labour_hours = 3 * number_heated_rooms
        labour_days = np.ceil(labour_hours / 8)

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }

    def celect_type_controls(self):
        """
        Calculate the cost of installing Celect type controls
        """

        # The £50 cost is a rough estimate based on internet research
        total_cost = 50
        subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
        vat = total_cost - subtotal_before_vat

        # We estimate the labour hours to be 4
        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": 4,
            "labour_days": 1,
        }

    def hot_water_tank_insulation(self):
        """
        Calculate the cost of installing hot water tank insulation
        """

        # The £50 cost is a rough estimate based on internet research
        total_cost = 50
        subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
        vat = total_cost - subtotal_before_vat

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": 0,
            "labour_days": 0,
        }

    def roomstat_programmer_trvs(
        self, number_heated_rooms, has_programmer, has_trvs, has_room_thermostat
    ):
        """

        :return:
        """

        total_cost = 0
        labour_hours = 0

        if not has_programmer:
            total_cost += PROGRAMMER_COST
            labour_hours += 1

        if not has_trvs:
            total_cost += TRVS_COST * number_heated_rooms
            labour_hours += 0.25 * number_heated_rooms

        if not has_room_thermostat:
            total_cost += ROOM_THERMOSTAT_COST
            labour_hours += 0.5

        subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
        vat = total_cost - subtotal_before_vat

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": labour_hours,
            "labour_days": 1,
        }

    def time_and_temperature_zone_control(self, number_heated_rooms):

        # The product costs are inclusive of VAT
        product_costs = (
            TTZC_SMART_THERMOSTAT_COST +
            TTZC_ROOM_TEMPERATURE_SENSOR_COST * number_heated_rooms +
            TTZC_SMART_RADIATOR_VALUES * number_heated_rooms
        )
        labour_hours = (
            TTZC_SMART_THERMOSTAT_LABOUR_HOURS +
            TTZC_ROOM_TEMPERATURE_SENSOR_LABOUR_HOURS * number_heated_rooms +
            TTZC_SMART_RADIATOR_VALUES_LABOUR_HOURS * number_heated_rooms
        )
        labour_costs = TTZC_ELECTRICIAN_HOURLY_RATE * labour_hours
        # Add continency and preliminaries to the labour to account for the complexity of the job
        labour_costs = labour_costs * (1 + self.CONTINGENCY + self.PRELIMINARIES)

        vat = labour_costs * self.VAT_RATE

        subtotal_before_vat = product_costs + labour_costs
        total_cost = subtotal_before_vat + vat

        labour_days = np.ceil(labour_hours / 8)

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }

    def heater_removal(self, n_rooms):
        """
        Estimates the costs of removal of heaters, including the redecoration costs of the space behind the heater
        :return:
        """

        removal_cost = ROOM_HEATER_REMOVAL_COST * n_rooms
        removal_labour_hours = ROOM_HEATER_REMOVAL_LABOUR_HOURS * n_rooms

        vat = removal_cost * self.VAT_RATE

        subtotal_before_vat = removal_cost
        total_cost = subtotal_before_vat + vat

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": removal_labour_hours,
            "labour_days": np.ceil(removal_labour_hours / 8),
        }

    @staticmethod
    def _estimate_n_radiators(number_habitable_rooms, total_floor_area, property_type, built_form):
        # Base number of radiators: one per habitable room
        base_radiators = number_habitable_rooms

        # Additional radiators for non-habitable essential areas (e.g., kitchens, hallways)
        additional_radiators = 3  # Initial assumption

        # Adjust additional radiators based on property type
        if property_type == 'Flat':
            additional_radiators -= 1  # Flats may need fewer radiators due to less exposure
        elif property_type in ['House', 'Bungalow', 'Maisonette']:
            # Multiple floors in Maisonette may require additional heating points
            additional_radiators += 2  # Houses and bungalows might need more due to greater exposure
        else:
            raise Exception("Invalid property type")

        # Adjust total radiator needs based on built form
        form_factor = {
            'Mid-Terrace': 0.95,
            'Semi-Detached': 1.05,
            'Detached': 1.25,
            'End-Terrace': 1.05
        }

        # Calculate total heating power needed and number of radiators based on standard output
        total_heating_power_required = total_floor_area * 80  # Watts per square meter
        radiator_output = 1000  # Average wattage per radiator
        total_radiators_based_on_power = (total_heating_power_required / radiator_output) * form_factor[built_form]

        # Final estimation taking the higher of calculated needs or base room count
        estimated_radiators = max(total_radiators_based_on_power, base_radiators + additional_radiators)
        return round(estimated_radiators)

    def boiler(self, size, exising_room_heaters, system_change, n_heated_rooms, n_rooms):
        """
        Based on a basic estimate of median value £2600 to install a low carbon combi boiler
        First time central heating vosts can als be found here:
        https://www.checkatrade.com/blog/cost-guides/central-heating-installation-cost/
        :return:
        """

        unit_cost = CONDENSING_BOILER_COSTS[size]
        # The unit cost is the cost without VAT
        # We now need to estimate the cost of the works
        labour_days = 2
        labour_hours = labour_days * 8
        labour_rate = 300

        # Average cost of installation is 1 (maybe 2days) at £300 per day
        # https://www.checkatrade.com/blog/cost-guides/new-boiler-cost/
        # To be pessimistic, assume 2 days work
        labour_cost = labour_rate * self.labour_adjustment_factor * labour_days
        # Add contingency and preliminaries
        labour_cost = labour_cost * (1 + self.CONTINGENCY + self.PRELIMINARIES)

        # labour_days = labour_days + (removal_labour_hours / 8)

        vat = labour_cost * self.VAT_RATE

        subtotal_before_vat = unit_cost + labour_cost
        total_cost = subtotal_before_vat + vat

        # if there are existing room heaters, we need to add the cost of removing them
        if exising_room_heaters:
            removal_costing = self.heater_removal(n_rooms=n_heated_rooms)
            # Add the totals to the existing totals
            total_cost += removal_costing["total"]
            subtotal_before_vat += removal_costing["subtotal"]
            labour_hours += removal_costing["labour_hours"]
            labour_days += removal_costing["labour_days"]
            vat += removal_costing["vat"]

        if system_change:
            # We need the cost of radiators
            n_radiators = self._estimate_n_radiators(
                number_habitable_rooms=n_rooms,
                total_floor_area=self.property.floor_area,
                property_type=self.property.data["property-type"],
                built_form=self.property.data["built-form"]
            )

            additionals_labour_cost = labour_rate * self.labour_adjustment_factor
            radiator_cost = DOUBLE_RADIATOR_COST * n_radiators
            system_change_cost = radiator_cost + FLUE_COST + PIPEWORK_COST + additionals_labour_cost
            system_change_cost_before_vat = system_change_cost / (1 + self.VAT_RATE)
            system_change_vat = system_change_cost - system_change_cost_before_vat
            # We add an extra labour day for the system change
            labour_days += 1
            labour_hours += 8
            total_cost += system_change_cost
            subtotal_before_vat += system_change_cost_before_vat
            vat += system_change_vat

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }

    def air_source_heat_pump(self):
        """
        Based on the region and type of property, this function will produce a cost estimation for an air source heat
        pump. This cost will include the boiler upgrade scheme grant

        """

        # This is the average cost of a project, we'll add some additional contingency
        regional_cost = MCS_AIR_SOURCE_HEAT_PUMP_COST_DATA[self.region]

        total_cost = regional_cost * (1 + self.CONTINGENCY) - BOILER_UPGRADE_SCHEME_ASHP_VALUE
        subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
        vat = total_cost - subtotal_before_vat

        # We assume 3 days installation
        labour_days = 3
        labour_hours = labour_days * 8

        return {
            "total": total_cost,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }
