import numpy as np
import pandas as pd
import backend.app.assumptions as assumptions

QUARTERLY_ENERGY_PRICES = [
    # 2024 Q1
    {"start": "2024-01-01", "end": "2024-03-31", "electricity": 0.2, "gas": 0.042},
    # 2023 Q4
    {"start": "2023-10-01", "end": "2023-12-31", "electricity": 0.202, "gas": 0.51},
    # 2023 Q3
    {"start": "2023-07-01", "end": "2023-09-30", "electricity": 0.188, "gas": 0.46},
    # 2023 Q2
    {"start": "2023-04-01", "end": "2023-06-30", "electricity": 0.177, "gas": 0.456},
]


class AnnualBillSavings:
    """
    This is a simple class which will estimate the annual bill savings, based on the kwh savings.
    This class uses data from Ofgem, including their price caps, to provide us with an estimate for
    1KWH of energy.
    """

    # These gas an electricity consumption figures are based off of figures presented by Ofgem
    # https://www.ofgem.gov.uk/information-consumers/energy-advice-households/average-gas-and-electricity-use-explained
    AVERAGE_ELECTRICITY_CONSUMPTION = 2700
    AVERAGE_GAS_CONSUMPTION = 11500

    # Latest price cap figures from Ofgem are for Jan 2026 to March 2026
    # https://www.ofgem.gov.uk/energy-price-cap
    ELECTRICITY_PRICE_CAP = 0.2769
    GAS_PRICE_CAP = 0.0593
    # This is the most recent export payment figure, at 13p/kWh - Updated Nov 2025
    # Smart export guarantee rates can be found here:
    # https://www.sunsave.energy/solar-panels-advice/exporting-to-the-grid/best-seg-rates
    ELECTRICITY_EXPORT_PAYMENT = 0.13

    # This is a weighted mean of the price caps, using the consumption figures above as weights
    PRICE_FACTOR = 0.09549999999999999

    # Daily standard charge, based on average across England, Scotland and Wales, and includes VAT
    DAILY_STANDARD_CHARGE_GAS = 0.3509
    DAILY_STANDARD_CHARGE_ELECTRICITY = 0.5475

    # Based on https://www.nottenergy.com/advice-and-tools/project-energy-cost-comparison
    # For July 2024. These quotes are based on the east midlands region, so we
    FUEL_DATA = pd.DataFrame([
        {"Fuel": "Electricity Standard", "Price (p)": 28.58, "Unit": "kWh", "Boiler Efficiency (%)": 100,
         "Energy Content, Net Calorific value (kWh/unit)": 1.00, "Price per kWh (p) (inc boiler efficiency)": 28.58,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.275},
        {"Fuel": "Mains Gas Standard", "Price (p)": 6.31, "Unit": "kWh", "Boiler Efficiency (%)": 90,
         "Energy Content, Net Calorific value (kWh/unit)": 1.00, "Price per kWh (p) (inc boiler efficiency)": 7.01,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.213},
        {"Fuel": "Kerosene", "Price (p)": 62.49, "Unit": "Litre", "Boiler Efficiency (%)": 90,
         "Energy Content, Net Calorific value (kWh/unit)": 9.79, "Price per kWh (p) (inc boiler efficiency)": 7.09,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.298},
        {"Fuel": "Gas oil", "Price (p)": 94.50, "Unit": "Litre", "Boiler Efficiency (%)": 90,
         "Energy Content, Net Calorific value (kWh/unit)": 9.96, "Price per kWh (p) (inc boiler efficiency)": 10.54,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.316},
        {"Fuel": "LPG", "Price (p)": 55.00, "Unit": "Litre", "Boiler Efficiency (%)": 90,
         "Energy Content, Net Calorific value (kWh/unit)": 6.78, "Price per kWh (p) (inc boiler efficiency)": 9.01,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.240},
        {"Fuel": "Butane", "Price (p)": 216.58, "Unit": "Litre", "Boiler Efficiency (%)": 90,
         "Energy Content, Net Calorific value (kWh/unit)": 6.64, "Price per kWh (p) (inc boiler efficiency)": 36.24,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.248},
        {"Fuel": "Propane", "Price (p)": 157.67, "Unit": "Litre", "Boiler Efficiency (%)": 90,
         "Energy Content, Net Calorific value (kWh/unit)": 7.22, "Price per kWh (p) (inc boiler efficiency)": 24.25,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.239},
        {"Fuel": "Kiln Dried (logs)", "Price (p)": 36.52, "Unit": "kg", "Boiler Efficiency (%)": 85,
         "Energy Content, Net Calorific value (kWh/unit)": 4.09, "Price per kWh (p) (inc boiler efficiency)": 10.51,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.024},
        {"Fuel": "Pellets (Bagged)", "Price (p)": 39.62, "Unit": "kg", "Boiler Efficiency (%)": 90,
         "Energy Content, Net Calorific value (kWh/unit)": 4.80, "Price per kWh (p) (inc boiler efficiency)": 9.17,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.049},
        {"Fuel": "Pellets (Blown bulk)", "Price (p)": 33.92, "Unit": "kg", "Boiler Efficiency (%)": 90,
         "Energy Content, Net Calorific value (kWh/unit)": 4.80, "Price per kWh (p) (inc boiler efficiency)": 7.85,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.049},
        {"Fuel": "Smokeless fuel", "Price (p)": 67.26, "Unit": "kg", "Boiler Efficiency (%)": 75,
         "Energy Content, Net Calorific value (kWh/unit)": 6.70, "Price per kWh (p) (inc boiler efficiency)": 13.38,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.404},
        {"Fuel": "Coal", "Price (p)": 48.50, "Unit": "kg", "Boiler Efficiency (%)": 75,
         "Energy Content, Net Calorific value (kWh/unit)": 7.95, "Price per kWh (p) (inc boiler efficiency)": 8.13,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.404},
        {"Fuel": "GSHP", "Price (p)": 28.58, "Unit": "kWh", "Boiler Efficiency (%)": 350,
         "Energy Content, Net Calorific value (kWh/unit)": 1.00, "Price per kWh (p) (inc boiler efficiency)": 8.17,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.079},
        {"Fuel": "ASHP", "Price (p)": 28.58, "Unit": "kWh", "Boiler Efficiency (%)": 294,
         "Energy Content, Net Calorific value (kWh/unit)": 1.00, "Price per kWh (p) (inc boiler efficiency)": 9.72,
         "CO2eq emission factor kgCO2eq/kWh (Gross CV)": 0.094}
    ])

    EPC_BANDS = ["G", "F", "E", "D", "C", "B", "A"]

    @classmethod
    def estimate(cls, kwh: float):
        """
        Estimate the annual bill savings based on the kwh savings
        :param kwh: The kwh savings
        :return: An estimate for annual bill savings
        """
        return cls.PRICE_FACTOR * kwh

    @classmethod
    def estimate_electric(cls, kwh: float):
        """
        Estimate the annual bill savings based on the kwh savings
        :param kwh: The kwh savings
        :return: An estimate for annual bill savings
        """
        return cls.ELECTRICITY_PRICE_CAP * kwh

    @classmethod
    def calculate_annual_bill(cls, kwh, mains_gas=True):
        """
        This method will estimate the total annual bill for a property
        It assumed gas & electricity are used
        :param kwh: The total kwh consumption
        :param mains_gas: Whether the property uses mains gas
        :return: An estimate for annual bill
        """

        if mains_gas:
            return cls.PRICE_FACTOR * kwh + (
                cls.DAILY_STANDARD_CHARGE_GAS + cls.DAILY_STANDARD_CHARGE_ELECTRICITY * 365)

        return cls.ELECTRICITY_PRICE_CAP * kwh + (cls.DAILY_STANDARD_CHARGE_ELECTRICITY * 365)

    @staticmethod
    def calculate_occupants(total_floor_area):
        """
        From Table 1b of the SAP 2012 documentation https://bregroup.com/documents/d/bre-group/sap-2012_9-92
        Provides a methodology to estimate occupancy, based on floor area. This is used to calculate the amount of
        electricity used be appliances and during cooking.
        :param total_floor_area:
        :return:
        """

        if total_floor_area <= 13.9:
            return 1

        return 1 + (1.76 * (1 - np.exp(-0.000349 * (total_floor_area - 13.9) * (total_floor_area - 13.9))) + 0.0013 * (
            total_floor_area - 13.9))

    @staticmethod
    def estimate_electrical_appliances(occupants, total_floor_area):
        """
        From secion L2 of SAP2012 Electrical appliances
        https://bregroup.com/documents/d/bre-group/sap-2012_9-92
        Used to estimate the amount of energy used by electrical appliances
        :param occupants:
        :param total_floor_area:
        :return:
        """
        e_a = 207.8 * np.power(total_floor_area * occupants, 0.4717)

        days_in_month = {
            1: 31,
            2: 28,
            3: 31,
            4: 30,
            5: 31,
            6: 30,
            7: 31,
            8: 31,
            9: 30,
            10: 31,
            11: 30,
            12: 31
        }

        eam = 0
        for m in range(1, 13):
            nm = days_in_month[m]
            eam += e_a * (1 + 0.157 * np.cos(2 * np.pi * (m - 1.78) / 12)) * nm / 365

        return eam

    @classmethod
    def estimate_appliances_energy_use(cls, total_floor_area):
        # The EPC energy consumption does not factor in cooking and applicance use, so this is estimated using the
        # methodology outlined in SAP, and is discussed in the UCL paper in section 3.1.1
        estimated_occupants = cls.calculate_occupants(total_floor_area=total_floor_area)
        appliances_energy_use = cls.estimate_electrical_appliances(estimated_occupants, total_floor_area)
        return appliances_energy_use

    @classmethod
    def adjust_energy_to_metered(cls, epc_energy, current_epc_rating):
        """
        The over-prediction of energy use by EPCs in Great Britain: A comparison
        of EPC-modelled and metered primary energy use intensity

        Which can be found here: https://www.sciencedirect.com/science/article/pii/S0378778823002542
        We implement the results on page 10

        This is used to just re-map the cost from the EPC to the metered cost
        epc_energy could be cost or kwh
        :return:
        """

        gradients = {
            "A": -0.1,
            "B": -0.1,
            "C": -0.43,
            "D": -0.52,
            "E": -0.7,
            "F": -0.76,
            "G": -0.76
        }

        intercepts = {
            "A": 28,
            "B": 28,
            "C": 97,
            "D": 119,
            "E": 160,
            "F": 157,
            "G": 157
        }

        gradient = gradients[current_epc_rating]
        intercept = intercepts[current_epc_rating]

        # This should be negative
        consumption_difference = gradient * epc_energy + intercept
        consumption_difference = 0 if consumption_difference > 0 else consumption_difference

        adjusted_consumption = (epc_energy + consumption_difference)
        if adjusted_consumption < 0:
            raise ValueError("consumption_difference should be negative")

        return adjusted_consumption

    @classmethod
    def adjust_expected_band(cls, expected_epc_rating, current_epc_rating):
        """
        Because of the differing intercepts and intercepts when adjusting, it's possible for
        expected_adjusted_energy to be bigger than current_adjusted_energy. In this case, we'll
        adjust, against at most 1 EPC band above the curent. This function performs the EPC adjustment
        :param expected_epc_rating: The expected EPC rating
        :param current_epc_rating: The current EPC rating
        """

        # Find index of expected EPC rating
        expected_index = cls.EPC_BANDS.index(expected_epc_rating)
        current_index = cls.EPC_BANDS.index(current_epc_rating)

        if expected_index - 1 < current_index:
            return current_epc_rating

        return cls.EPC_BANDS[expected_index - 1]

    @staticmethod
    def cost_per_kwh(price_per_unit, energy_content_per_unit):
        """
        Calculate the cost of fuel per kWh given the price per unit in GBP and the energy content per unit in kWh.
        """
        cost_per_kwh = price_per_unit / energy_content_per_unit
        # Tgis data is returned in pennies so we convert to pounds
        return cost_per_kwh / 100

    @classmethod
    def calculate_recommendation_fuel_cost(cls, kwh, fuel, cop):
        if fuel == "Electricity":
            return (kwh / cop) * cls.ELECTRICITY_PRICE_CAP

        # We handle "Unmapped" in a similar fashion to gas
        if fuel in ["Natural Gas", "Natural Gas (Community Scheme)", "Unmapped"]:
            return (kwh / cop) * cls.GAS_PRICE_CAP

        if fuel == "LPG":
            # Get the cost per kwh
            price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "LPG"].squeeze()
            cost_per_kwh = cls.cost_per_kwh(
                price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"]
            )
            return (kwh / cop) * cost_per_kwh

        if fuel in ["Wood Logs", "Wood Pellets"]:
            price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "Pellets (Bagged)"].squeeze()
            cost_per_kwh = cls.cost_per_kwh(
                price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"]
            )
            return (kwh / cop) * cost_per_kwh

        if fuel == "Natural Gas + Solar Thermal":
            # The solar thermal covers a % of the heating kwh, so we need to adjust the cost
            return (kwh / cop) * assumptions.SOLAR_CONSUMPTION_PROPORTION * cls.GAS_PRICE_CAP

        if fuel in ["Electricity + Solar Thermal", 'Unmapped + Solar Thermal']:
            # The solar thermal covers a % of the heating kwh, so we need to adjust the cost
            return (kwh / cop) * assumptions.SOLAR_CONSUMPTION_PROPORTION * cls.ELECTRICITY_PRICE_CAP

        if fuel in ['Oil + Solar Thermal']:
            # The solar thermal covers a % of the heating kwh, so we need to adjust the cost
            price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "Kerosene"].squeeze()
            cost_per_kwh = cls.cost_per_kwh(
                price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"]
            )
            return (kwh / cop) * cost_per_kwh * assumptions.SOLAR_CONSUMPTION_PROPORTION

        if fuel == "LPG + Solar Thermal":
            # The solar thermal covers a % of the heating kwh, so we need to adjust the cost
            price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "LPG"].squeeze()
            cost_per_kwh = cls.cost_per_kwh(
                price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"]
            )
            return (kwh / cop) * cost_per_kwh * assumptions.SOLAR_CONSUMPTION_PROPORTION

        if fuel == "Oil":
            price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "Kerosene"].squeeze()
            cost_per_kwh = cls.cost_per_kwh(
                price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"]
            )
            return (kwh / cop) * cost_per_kwh

        if fuel in ["Smokeless Fuel", "Anthracite"]:
            price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "Smokeless fuel"].squeeze()
            cost_per_kwh = cls.cost_per_kwh(
                price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"]
            )
            return (kwh / cop) * cost_per_kwh

        # We use coal's values for
        if fuel == "Coal":
            price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "Coal"].squeeze()
            cost_per_kwh = cls.cost_per_kwh(
                price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"]
            )
            return (kwh / cop) * cost_per_kwh

        raise Exception("Fuel not recognised")
