import numpy as np

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 April 2024
    # https://www.ofgem.gov.uk/publications/new-energy-price-cap-level-april-june-2024-starts-today
    ELECTRICITY_PRICE_CAP = 0.245
    GAS_PRICE_CAP = 0.0604
    # This is the most recent export payment figure, at 12p per kwh
    ELECTRICITY_EXPORT_PAYMENT = 0.12

    # 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.3143
    DAILY_STANDARD_CHARGE_ELECTRICITY = 0.601

    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 adjust_energy_to_metered(cls, epc_energy_consumption, current_epc_rating, total_floor_area):
        """
        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

        :return:
        """

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

        epc_energy_consumption += appliances_energy_use

        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_consumption + intercept

        adjusted_consumption = (epc_energy_consumption + 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]
