from typing import Mapping, Any
import numpy as np

from recommendations.county_to_region import county_to_region_map
from utils.logger import setup_logger
from backend.ml_models.AnnualBillSavings import AnnualBillSavings

logger = setup_logger()

# 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},
]

# Installers are now working with 435 watt panels
PANEL_SIZE = 0.435

INSTALLER_SOLAR_COSTS = [
    {"n_panels": 4, "array_kwp": 4 * PANEL_SIZE, "cost": 4089.25, "installer": "CEG"},
    {"n_panels": 5, "array_kwp": 5 * PANEL_SIZE, "cost": 4242.48, "installer": "CEG"},
    {"n_panels": 6, "array_kwp": 6 * PANEL_SIZE, "cost": 4395.71, "installer": "CEG"},
    {"n_panels": 7, "array_kwp": 7 * PANEL_SIZE, "cost": 4548.94, "installer": "CEG"},
    {"n_panels": 8, "array_kwp": 8 * PANEL_SIZE, "cost": 4702.17, "installer": "CEG"},
    {"n_panels": 9, "array_kwp": 9 * PANEL_SIZE, "cost": 4855.41, "installer": "CEG"},
    {"n_panels": 10, "array_kwp": 10 * PANEL_SIZE, "cost": 5010.95, "installer": "CEG"},
    {"n_panels": 11, "array_kwp": 11 * PANEL_SIZE, "cost": 5166.49, "installer": "CEG"},
    {"n_panels": 12, "array_kwp": 12 * PANEL_SIZE, "cost": 5322.04, "installer": "CEG"},
    {"n_panels": 13, "array_kwp": 13 * PANEL_SIZE, "cost": 5657.6, "installer": "CEG"},
    {"n_panels": 14, "array_kwp": 14 * PANEL_SIZE, "cost": 5993.16, "installer": "CEG"},
    {"n_panels": 15, "array_kwp": 15 * PANEL_SIZE, "cost": 6328.71, "installer": "CEG"},
    {"n_panels": 16, "array_kwp": 16 * PANEL_SIZE, "cost": 6483.33, "installer": "CEG"},
    {"n_panels": 17, "array_kwp": 17 * PANEL_SIZE, "cost": 6637.95, "installer": "CEG"},
    {"n_panels": 18, "array_kwp": 18 * PANEL_SIZE, "cost": 6792.57, "installer": "CEG"},
]

# These are costs we received from CRG, for pricing up air source heat pumps
# These are costs that we have been provided from CRG specifically for air source heat pumps
ASHP_SMALL_SYSTEM_COST = 8812.92  # 4.8 to 8.5, based on their pricing
ASHP_LARGE_SYSTEM_COST = 11053.25
ASHP_SECURITY = 455.00
ASHP_WALL_BRACKET = 574.17
ASHP_DISTRIBUTION_SYSTEM_COSTS = [
    {"n_radiators": 4, "cost": 3380.00},
    {"n_radiators": 5, "cost": 3607.50},
    {"n_radiators": 6, "cost": 4116.67},
    {"n_radiators": 7, "cost": 4647.50},
    {"n_radiators": 8, "cost": 5200.00},
    {"n_radiators": 9, "cost": 5730.83},
    {"n_radiators": 10, "cost": 6283.33},
    {"n_radiators": 11, "cost": 6857.50},
    {"n_radiators": 12, "cost": 7431.67},
    {"n_radiators": 13, "cost": 8016.67},
    {"n_radiators": 14, "cost": 8612.50},
    {"n_radiators": 15, "cost": 9219.17},
    {"n_radiators": 16, "cost": 9804.17},
    {"n_radiators": 17, "cost": 10389.17},
]
ASHP_CYLINDER_COSTS = [
    {"capacity_l": 120, "cost": 3318.25},
    {"capacity_l": 180, "cost": 3480.75},
    {"capacity_l": 200, "cost": 3853.42},
    {"capacity_l": 250, "cost": 3961.75},
]

# CEG uses use Solshare as an inverter to provide solar PV to multiple flats. This costs £7500 for the inverter alone
# https://midsummerwholesale.co.uk/buy/solshare
INSTALLER_SOLAR_PV_INVERTER_COST = 7500
INSTALLER_SOLAR_PV_INVERTER_LABOUR_COST = 500  # Just a rough guess to labour costs

INSTALLER_SOLAR_BATTERY_COSTS = [
    {
        "capacity_kwh": 5,
        "description": "Battery Add on",
        "cost": 3769.89,
        "installer": "JJC",
    },
    # {'capacity_kwh': 10, 'description': 'Battery Add on', 'cost': 4300.00, 'installer': 'CEG'},
    # {'capacity_kwh': 5, 'description': 'Battery Retrofit existing system', 'cost': 4250.00, 'installer': 'CEG'},
    # {'capacity_kwh': 10, 'description': 'Battery Retrofit Existing system', 'cost': 5950.00, 'installer': 'CEG'}
]

# 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
BYPASS_COST = 350  # Based on desktop research for a complex installation
# https://www.checkatrade.com/blog/cost-guides/cost-install-water-shut-off-valve/

# 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)
)

# boiler prices based on
# This is the cost of a firs time central heating install from The Warm Front rate card
# These are exclusive of installation costs
CONDENSING_BOILER_COST = 2600

# Electric boiler prices base on
# https://www.greenmatch.co.uk/boilers/combi-boilers/electric-combi-boilers
# https://www.tlc-direct.co.uk/Products/ERMAC15.html
# The unit is a 15kw boiler, capable of outputting between 3kw and 15kw. Costs seem to be around £1800
ELECTRIC_BOILER_COSTS = 1800

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

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

    # Measure level contingency
    CONTINGENCIES = {
        "cavity_wall_insulation": 0.1,
        "internal_wall_insulation": 0.26,
        "external_wall_insulation": 0.26,
        "loft_insulation": 0.1,
        "solar_pv": 0.15,
        "air_source_heat_pump": 0.25,
        "flat_roof_insulation": 0.26,
        "suspended_floor_insulation": 0.2,
        "solid_floor_insulation": 0.26,
        "low_energy_lighting": 0.26,
        "high_heat_retention_storage_heaters": 0.1,
        "windows_glazing": 0.15,
        "boiler_upgrade": 0.26,
        "time_and_temperature_zone_control": 0.1,
        "roomstat_programmer_trvs": 0.1,
        "room_roof_insulation": 0.26,
        "heater_removal": 0.1,
        "sealing_open_fireplace": 0.1,
        "mechanical_ventilation": 0.26,
        "sloping_ceiling_insulation": 0.26,  # Similar to IWI so using the same contingency
    }

    # 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

    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

    # Based on relative costs from SCIS
    SECONDARY_GLAZING_SCALING_FACTOR = 0.85

    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.epc_record.county, None)
        if self.region is None:
            # Try and grab using the local-authority-label
            self.region = county_to_region_map.get(
                self.property.epc_record.local_authority_label, None
            )

            if self.region is None:
                # Try and get the region after converting the keys to lower
                if self.property.epc_record.local_authority_label is not None:
                    self.region = {
                        k.lower(): v for k, v in county_to_region_map.items()
                    }.get(self.property.epc_record.local_authority_label.lower(), None)

            if self.region is None:
                logger.warning(
                    "No region found for county %s, defaulting to South East England",
                    self.property.epc_record.county,
                )
                self.region = "South East England"

        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

        total_cost = material["total_cost"] * wall_area

        return {
            "total": total_cost,
            "contingency": self.CONTINGENCIES["cavity_wall_insulation"] * total_cost,
            "contingency_rate": self.CONTINGENCIES["cavity_wall_insulation"],
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }

    def loft_and_flat_insulation(self, floor_area, material):
        """
        Calculates the total cost for loft/flat roof insulation based on material and labor costs,
        including contingency, preliminaries, profit, and VAT.

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

        total_cost = material["total_cost"] * floor_area
        if material["type"] == "loft_insulation":
            contingency_rate = self.CONTINGENCIES["loft_insulation"]
            contingency = contingency_rate * total_cost
        else:
            contingency_rate = self.CONTINGENCIES["flat_roof_insulation"]
            contingency = contingency_rate * total_cost

        return {
            "total": total_cost,
            "contingency": contingency,
            "contingency_rate": contingency_rate,
            "labour_hours": 8,
            "labour_days": 1,
        }

    def solid_wall_insulation(self, wall_area, material):
        """
        Implements costing methodology now that we have direct quotes from installers.
        :return:
        """

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

        if material["type"] == "internal_wall_insulation":
            contingency_rate = self.CONTINGENCIES["internal_wall_insulation"]
        else:
            contingency_rate = self.CONTINGENCIES["external_wall_insulation"]

        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,
            "contingency": contingency_rate * total_cost,
            "contingency_rate": contingency_rate,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }

    def suspended_floor_insulation(self, insulation_floor_area, material):
        """
        Given an installer cost for the works, produces an estimate for the cost of works.
        Includes contingency
        """

        # if the material is based on an installer cost, we return the flat price
        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,
            "contingency": self.CONTINGENCIES["suspended_floor_insulation"]
            * total_cost,
            "contingency_rate": self.CONTINGENCIES["suspended_floor_insulation"],
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }

    @staticmethod
    def _estimate_number_of_days_for_solid_floor(insulation_floor_area: float) -> float:
        """
        Estimate the number of labour days required to install solid floor insulation,
        based on the floor area being treated.

        This is a heuristic (rule-of-thumb) estimate designed for early-stage planning
        and costing. It deliberately avoids strict linear scaling because real-world
        construction work includes fixed overheads and efficiency gains.

        Core assumptions:
        - A typical solid floor insulation job covering ~45 m2 takes around 7 working days.
          This is based on market guidance (e.g. Checkatrade).
        - Very small jobs still require multiple days due to setup, preparation,
          curing/drying time, and inspections — even if the area is small.
        - Larger jobs take longer, but each additional square metre adds slightly less
          time than the previous one, because crews become more efficient once work
          is underway.

        The estimate therefore:
        - Scales with floor area
        - Applies a minimum realistic duration
        - Uses non-linear scaling to reflect economies of scale

        :param insulation_floor_area: float - total floor area to be insulated
        """
        # Reference case:
        # A "typical" job (about half of a 90 m² house) takes ~7 days to complete
        base_days = 7
        base_area = 45  # m2 of solid floor insulated in the reference case

        # Exponent < 1 means sub-linear scaling:
        # doubling the area does NOT double the time, because setup costs
        # and learning effects reduce the marginal effort per extra m²
        labour_exponent = 0.85

        # Minimum number of days for any solid floor job.
        # Even small areas require mobilisation, preparation, installation,
        # and finishing time, so jobs realistically won't complete faster than this.
        min_days = 3

        # Calculate estimated labour days:
        # - Scale relative to the reference job
        # - Apply sub-linear scaling for realism
        # - Enforce a minimum duration so estimates are not unrealistically low
        labour_days = max(
            min_days, base_days * (insulation_floor_area / base_area) ** labour_exponent
        )

        return labour_days

    def solid_floor_insulation(self, insulation_floor_area, material):
        """
        based on costing data from installers, produces an estimate for the cost of works. Returns contingency

        :param insulation_floor_area: Area of the floor to be insulated
        :param material: Selected insulation material
        :return:
        """

        total_cost = material["total_cost"] * insulation_floor_area
        daily_labour_rate = 300  # Based on checkatrade

        labour_days = self._estimate_number_of_days_for_solid_floor(
            insulation_floor_area
        )
        labour_cost = labour_days * daily_labour_rate

        total_cost = total_cost + labour_cost

        total_cost = round(total_cost)

        return {
            "total": total_cost,
            "contingency": self.CONTINGENCIES["solid_floor_insulation"] * total_cost,
            "contingency_rate": self.CONTINGENCIES["solid_floor_insulation"],
            "labour_hours": labour_days * 8,
            "labour_days": labour_days,
        }

    def low_energy_lighting(self, number_of_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
        :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

        total_cost = material["total_cost"] * number_of_lights

        labour_hours = 1
        labour_days = labour_hours / 8

        return {
            "total": total_cost,
            "contingency": self.CONTINGENCIES["low_energy_lighting"] * total_cost,
            "contingency_rate": self.CONTINGENCIES["low_energy_lighting"],
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }

    def window_glazing(self, number_of_windows, material, is_secondary_glazing=False):
        """
        Given an isntaller quote, produces an estimate for the cost of works.

        """

        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,
            "contingency": self.CONTINGENCIES["windows_glazing"] * total_cost,
            "contingency_rate": self.CONTINGENCIES["windows_glazing"],
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }

    @classmethod
    def solar_pv(cls, solar_product, scaffolding_options, n_floors):
        """ """

        system_cost = solar_product["total_cost"]

        if not solar_product["includes_scaffolding"]:
            # We base this on the number of floors
            scaffolding = [
                x["total_cost"] for x in scaffolding_options if x["size"] == n_floors
            ]
            if not scaffolding:
                # If we have no options, handle this
                if n_floors <= 3:
                    raise ValueError(
                        "No scaffolding options available for 3 or fewer floors"
                    )
                # We take the largest scaffolding option available
                scaffolding_cost = max([x["total_cost"] for x in scaffolding_options])
            else:
                scaffolding_cost = min(scaffolding)

            system_cost += scaffolding_cost

        # 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 48 hours of
        # labour
        return {
            "total": system_cost,
            "subtotal": system_cost,
            "contingency": system_cost * cls.CONTINGENCIES["solar_pv"],
            "contingency_rate": cls.CONTINGENCIES["solar_pv"],
            "vat": 0,
            "labour_hours": 48,
            "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,
            "contingency": total_cost * self.CONTINGENCY,
            "contingency_rate": self.CONTINGENCY,
            "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,
            "contingency": total_cost * self.CONTINGENCY,
            "contingency_rate": self.CONTINGENCY,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": labour_hours,
            "labour_days": labour_days,
        }

    def high_heat_electric_storage_heaters(
        self,
        number_heated_rooms: int,
        needs_cylinder: bool,
        product: dict | None = None,
    ):
        """
        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
        :param needs_cylinder: bool, whether the property needs a hot water cylinder
        :param product: dict, product data containing costs of heaters
        """

        if needs_cylinder:
            # 1500 is the cost of a new hot water cylinder
            total_cost = product["total_cost"] * number_heated_rooms + 1500
        else:
            # 500 is the cost of a dual immersion heater - a rough estimate
            total_cost = product["total_cost"] * number_heated_rooms + 500

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

        labour_hours = 3 * number_heated_rooms
        labour_days = np.ceil(labour_hours / 8)

        return {
            "total": total_cost,
            "contingency": total_cost
            * self.CONTINGENCIES["high_heat_retention_storage_heaters"],
            "contingency_rate": self.CONTINGENCIES[
                "high_heat_retention_storage_heaters"
            ],
            "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,
            "contingency": total_cost * self.CONTINGENCY,
            "contingency_rate": self.CONTINGENCY,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": 4,
            "labour_days": 1,
        }

    def cylinder_thermostat(self):
        """
        Calculate the cost of installing a cylinder thermostat
        """

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

        # We estimate the labour hours to be 2
        return {
            "total": total_cost,
            "contingency": total_cost * self.CONTINGENCY,
            "contingency_rate": self.CONTINGENCY,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": 2,
            "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,
            "contingency": total_cost * self.CONTINGENCY,
            "contingency_rate": self.CONTINGENCY,
            "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

        contingency_rate = self.CONTINGENCIES["roomstat_programmer_trvs"]

        return {
            "total": total_cost,
            "contingency": total_cost * contingency_rate,
            "contingency_rate": contingency_rate,
            "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)

        contingency_rate = self.CONTINGENCIES["time_and_temperature_zone_control"]

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

    def programmer_trvs_bypass(
        self, number_heated_rooms, has_programmer, has_trvs, has_bypass
    ):

        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_bypass:
            total_cost += BYPASS_COST
            labour_hours += 0.5

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

        return {
            "total": total_cost,
            "contingency": total_cost * self.CONTINGENCY,
            "contingency_rate": self.CONTINGENCY,
            "subtotal": subtotal_before_vat,
            "vat": vat,
            "labour_hours": labour_hours,
            "labour_days": 1,
        }

    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 + 200  # Adding a baseline £200 cost after commercial feedback
        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

        contingency_rate = self.CONTINGENCIES["heater_removal"]

        return {
            "total": total_cost,
            "contingency": total_cost * contingency_rate,
            "contingency_rate": contingency_rate,
            "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 = {
            "Enclosed Mid-Terrace": 0.9,
            "Mid-Terrace": 0.95,
            "Enclosed End-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,
        exising_room_heaters,
        system_change,
        n_heated_rooms,
        n_rooms,
        is_electric=False,
    ):
        """
        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:
        """

        if not is_electric:
            unit_cost = CONDENSING_BOILER_COST
        else:
            unit_cost = ELECTRIC_BOILER_COSTS
        # 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

        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.epc_record.property_type,
                built_form=self.property.epc_record.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

        contingency_rate = self.CONTINGENCIES["boiler_upgrade"]

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

    @staticmethod
    def _select_cylinder_capacity(occupants: float):
        if occupants <= 2:
            return 120
        elif occupants <= 3:
            return 180
        elif occupants <= 4:
            return 200
        else:
            return 250

    def air_source_heat_pump(
        self, ashp_size: float, number_heated_rooms: int, total_floor_area: float
    ) -> dict:
        """
        We produce a cost estimation for an air source heat pump, based on costs we have received from installers.

        """

        system_cost = (
            (ASHP_SMALL_SYSTEM_COST if ashp_size <= 8.5 else ASHP_LARGE_SYSTEM_COST)
            + ASHP_SECURITY
            + ASHP_WALL_BRACKET
        )

        available_n_rads = [x["n_radiators"] for x in ASHP_DISTRIBUTION_SYSTEM_COSTS]
        if number_heated_rooms < min(available_n_rads):
            # We use the smallest value
            rads_to_use = min(available_n_rads)
        elif number_heated_rooms > max(available_n_rads):
            # We use the largest value
            rads_to_use = max(available_n_rads)
        else:
            rads_to_use = int(number_heated_rooms)

        distribution_system_cost = [
            x for x in ASHP_DISTRIBUTION_SYSTEM_COSTS if x["n_radiators"] == rads_to_use
        ][0]["cost"]

        # Cylinder cost
        est_n_occupants = AnnualBillSavings.calculate_occupants(total_floor_area)
        cylinder_capacity = self._select_cylinder_capacity(est_n_occupants)
        cylinder_cost = [
            x for x in ASHP_CYLINDER_COSTS if x["capacity_l"] == cylinder_capacity
        ][0]["cost"]

        total = system_cost + distribution_system_cost + cylinder_cost

        return {
            "total": total,
            "contingency": total * self.CONTINGENCIES["air_source_heat_pump"],
            "contingency_rate": self.CONTINGENCIES["air_source_heat_pump"],
            "vat": 0,
            "labour_hours": 80,
            "labour_days": 10,
        }

    @staticmethod
    def _estimate_number_of_days_for_sloping_ceiling(
        insulation_roof_area: float,
    ) -> float:
        """
        Estimate labour days required to insulate an existing sloping ceiling.

        Heuristic model based on retrofit guidance (Checkatrade, The Green Age)
        and analogy with internal wall insulation.

        See _estimate_number_of_days_for_solid_floor for detailed explanation regarding assumptions
        and methodology, however for the purpose of placeholder, this function mimics the approach
        to that method but is detached to allow for future changes

        Assumptions:
        - ~30 m² of sloping ceiling takes ~4 working days
        - Small jobs still require multiple days (setup, stripping, reboarding)
        - Larger areas benefit from economies of scale, but not linearly

        :param insulation_roof_area: m² of sloping ceiling to be insulated
        """

        base_days = 4
        base_area = 30  # m2 reference case
        labour_exponent = 0.85
        min_days = 2

        labour_days = max(
            min_days, base_days * (insulation_roof_area / base_area) ** labour_exponent
        )

        return labour_days

    @classmethod
    def sloping_ceiling_insulation(
        cls, insulation_roof_area: float
    ) -> Mapping[str, float]:
        """
        This costing for this is based on Checkatrade desktop research, since we are yet to receive installer quotes.
        :param insulation_roof_area: Area of the sloping ceiling to be insulated
        :return:
        """
        ################
        # Assumptions
        ################
        # Sources:
        # https://www.checkatrade.com/blog/cost-guides/vaulted-ceiling-cost/
        # https://www.thegreenage.co.uk/can-i-insulate-my-sloping-ceiling/
        # These assumptions last updated 21/02/2026
        insulation_cost_per_m2 = (
            52  # The actual install process is quite similar to IWI
        )
        labour_rate = 250  # per day
        contingency_rate = cls.CONTINGENCIES["sloping_ceiling_insulation"]

        labour_days = cls._estimate_number_of_days_for_sloping_ceiling(
            insulation_roof_area
        )
        labour_hours = labour_days * 8

        total = (insulation_cost_per_m2 * insulation_roof_area) + (
            labour_rate * labour_days
        )

        # Assume VAT included in the total => total is 120% of subtotal
        vat = total - (total / 1.2)

        return {
            "total": float(total),
            "contingency": float(total * contingency_rate),
            "contingency_rate": contingency_rate,
            "vat": float(vat),
            "labour_hours": float(labour_hours),
            "labour_days": float(labour_days),
        }
