import pandas as pd
from numpy import nan
from typing import Union, Callable
from collections.abc import Mapping
from datatypes.epc.roof import EpcRoofDescriptions
from datatypes.epc.efficiency import EpcEfficiency
from datatypes.epc.construction_age_band import EpcConstructionAgeBand

roof_map = {
    # Dwelling above
    ('AnotherDwellingAbove', 'Another Dwelling Above'): EpcRoofDescriptions.another_dwelling_above,
    ('SameDwellingAbove', 'Same Dwelling Above'): EpcRoofDescriptions.another_dwelling_above,
    # Pitched, normal loft access, with a loft thickness
    ('PitchedNormalLoftAccess', 'mm25'): EpcRoofDescriptions.loft_25mm_insulation,
    ('PitchedNormalLoftAccess', 'mm50'): EpcRoofDescriptions.loft_50mm_insulation,
    ('PitchedNormalLoftAccess', 'mm75'): EpcRoofDescriptions.loft_75mm_insulation,
    ('PitchedNormalLoftAccess', 'mm100'): EpcRoofDescriptions.loft_100mm_insulation,
    ('PitchedNormalLoftAccess', 'mm150'): EpcRoofDescriptions.loft_150mm_insulation,
    ('PitchedNormalLoftAccess', 'mm200'): EpcRoofDescriptions.loft_200mm_insulation,
    ('PitchedNormalLoftAccess', 'mm250'): EpcRoofDescriptions.loft_250mm_insulation,
    ('PitchedNormalLoftAccess', 'mm270'): EpcRoofDescriptions.loft_270mm_insulation,
    ('PitchedNormalLoftAccess', 'mm300'): EpcRoofDescriptions.loft_300mm_insulation,
    ('PitchedNormalLoftAccess', 'mm350'): EpcRoofDescriptions.loft_350mm_insulation,
    ('PitchedNormalLoftAccess', 'mm400'): EpcRoofDescriptions.loft_400mm_plus_insulation,

    # Pitched, no loft access, with a loft thickness
    ('PitchedNormalNoLoftAccess', 'mm25'): EpcRoofDescriptions.loft_25mm_insulation,
    ('PitchedNormalNoLoftAccess', 'mm50'): EpcRoofDescriptions.loft_50mm_insulation,
    ('PitchedNormalNoLoftAccess', 'mm75'): EpcRoofDescriptions.loft_75mm_insulation,
    ('PitchedNormalNoLoftAccess', 'mm100'): EpcRoofDescriptions.loft_100mm_insulation,
    ('PitchedNormalNoLoftAccess', 'mm150'): EpcRoofDescriptions.loft_150mm_insulation,
    ('PitchedNormalNoLoftAccess', 'mm200'): EpcRoofDescriptions.loft_200mm_insulation,
    ('PitchedNormalNoLoftAccess', 'mm250'): EpcRoofDescriptions.loft_250mm_insulation,
    ('PitchedNormalNoLoftAccess', 'mm270'): EpcRoofDescriptions.loft_270mm_insulation,
    ('PitchedNormalNoLoftAccess', 'mm300'): EpcRoofDescriptions.loft_300mm_insulation,
    ('PitchedNormalNoLoftAccess', 'mm350'): EpcRoofDescriptions.loft_350mm_insulation,
    ('PitchedNormalNoLoftAccess', 'mm400'): EpcRoofDescriptions.loft_400mm_plus_insulation,

    # All pitched options with asbuilt or unknown got to EpcRoofDescriptions.pitched_insulated_assumed
    # With access
    ('PitchedNormalLoftAccess', nan): EpcRoofDescriptions.pitched_insulated_assumed,
    ('PitchedNormalLoftAccess', 'AsBuilt'): EpcRoofDescriptions.pitched_insulated_assumed,
    ('PitchedNormalLoftAccess', 'Unknown'): EpcRoofDescriptions.pitched_insulated_assumed,
    # No access
    ('PitchedNormalNoLoftAccess', nan): EpcRoofDescriptions.pitched_insulated_assumed,
    ('PitchedNormalNoLoftAccess', 'AsBuilt'): EpcRoofDescriptions.pitched_insulated_assumed,
    ('PitchedNormalNoLoftAccess', 'Unknown'): EpcRoofDescriptions.pitched_insulated_assumed,

    # Flat
    ('Flat', 'NoInsulation'): EpcRoofDescriptions.flat_no_insulation,
    # Flat - limited insulation
    ('Flat', '12mm'): EpcRoofDescriptions.flat_limited_insulation,
    ('Flat', 'mm25'): EpcRoofDescriptions.flat_limited_insulation,
    ('Flat', 'mm50'): EpcRoofDescriptions.flat_limited_insulation,
    # Flat insulated
    ('Flat', 'mm75'): EpcRoofDescriptions.flat_insulated,
    ('Flat', 'mm100'): EpcRoofDescriptions.flat_insulated,
    ('Flat', 'mm150'): EpcRoofDescriptions.flat_insulated,
    ('Flat', 'mm200'): EpcRoofDescriptions.flat_insulated,
    ('Flat', 'mm250'): EpcRoofDescriptions.flat_insulated,
    ('Flat', 'mm300'): EpcRoofDescriptions.flat_insulated,
    ('Flat', 'mm350'): EpcRoofDescriptions.flat_insulated,
    ('Flat', 'mm400'): EpcRoofDescriptions.flat_insulated,
    # Flat - as built or unknown
    ('Flat', 'AsBuilt'): None,  # To be classified
    ('Flat', nan): None,  # To be classified
    ('Flat', 'Unknown'): None,  # To be classified

    # 12mm = very poor & has limited insulation description
    # 25, 50 = poor & has limited insulation description
    # 75, 100, 125mm = average (Flat, insulated)
    # 150, 175, 200, 225, 250mm = good (Flat, insulated)
    # 270mm+ = very good (Flat, insulated)

    # Thatched
    ('PitchedThatched', 'mm50'): EpcRoofDescriptions.thatched_with_additional_insulation,
    ('PitchedThatched', 'mm150'): EpcRoofDescriptions.thatched_with_additional_insulation,
    ('PitchedThatched', 'mm300'): EpcRoofDescriptions.thatched_with_additional_insulation,
    ('PitchedThatched', 'Unknown'): EpcRoofDescriptions.thatched,  # efficiency classified based on age

    # Sloping:
    # Limited (12 very poor, 25-50 poor)
    ('PitchedWithSlopingCeiling', 'mm12'): EpcRoofDescriptions.sloping_pitched_limited_insulation,
    ('PitchedWithSlopingCeiling', 'mm25'): EpcRoofDescriptions.sloping_pitched_limited_insulation,
    ('PitchedWithSlopingCeiling', 'mm50'): EpcRoofDescriptions.sloping_pitched_limited_insulation,
    # Insulated 75mm+ (75 - 125 average, 150 - 250 good, 270+ very good)
    ('PitchedWithSlopingCeiling', 'mm75'): EpcRoofDescriptions.sloping_pitched_insulated,
    ('PitchedWithSlopingCeiling', 'mm100'): EpcRoofDescriptions.sloping_pitched_insulated,
    ('PitchedWithSlopingCeiling', 'mm150'): EpcRoofDescriptions.sloping_pitched_insulated,
    ('PitchedWithSlopingCeiling', 'mm200'): EpcRoofDescriptions.sloping_pitched_insulated,
    ('PitchedWithSlopingCeiling', 'mm250'): EpcRoofDescriptions.sloping_pitched_insulated,
    ('PitchedWithSlopingCeiling', 'mm270'): EpcRoofDescriptions.sloping_pitched_insulated,
    ('PitchedWithSlopingCeiling', 'mm300'): EpcRoofDescriptions.sloping_pitched_insulated,
    ('PitchedWithSlopingCeiling', 'mm350'): EpcRoofDescriptions.sloping_pitched_insulated,
    ('PitchedWithSlopingCeiling', 'mm400'): EpcRoofDescriptions.sloping_pitched_insulated,
    # As built/unknown
    ('PitchedWithSlopingCeiling', 'AsBuilt'): None,  # To be classified
    ('PitchedWithSlopingCeiling', nan): None,  # To be classified
    ('PitchedWithSlopingCeiling', 'Unknown'): None,  #
}

roof_unknown_age_fallback = {
    "Flat": EpcRoofDescriptions.flat_as_built_unknown,
    "PitchedWithSlopingCeiling": EpcRoofDescriptions.sloping_pitched_as_built_unknown,
    "PitchedThatched": EpcRoofDescriptions.thatched_as_built_unknown,
    "PitchedNormalLoftAccess": EpcRoofDescriptions.loft_as_built_unknown,
    "PitchedNormalNoLoftAccess": EpcRoofDescriptions.loft_as_built_unknown,
}

RoofEfficiencyRule = Union[
    EpcEfficiency,
    Callable[[EpcConstructionAgeBand, int | None], EpcEfficiency],
]


def flat_insulated_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
    """
    before 1900, 1900-1929, 1930-1949, 1950-1966, 1967-1975  -> Pitched, no insulation, Very Poor
    1976-1982 -> Pitched, limited insulation, Poor
    1983-1990, to 1996-2002 Pitched, insulated, Average
    2003 - 2006, 2012-2022 -> Pitched, insulated, Good
    2023 onwards -> Pitched, insulated, Very Good
    :param age_band: EpcConstructionAgeBand
    :return: EpcEfficiency
    """

    start_year = age_band.start_year()
    if start_year >= 2023:
        return EpcEfficiency.VERY_GOOD

    if start_year >= 2003:
        return EpcEfficiency.GOOD

    if start_year >= 1983:
        return EpcEfficiency.AVERAGE

    if start_year >= 1976:
        return EpcEfficiency.POOR

    return EpcEfficiency.VERY_POOR


def flat_insulated_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
    """
    12mm -> Very Poor
    25mm - 50mm -> Poor
    75mm - 125mm -> Pitched, insulated, average
    150mm - 250mm -> good
    270mm+ -> very good
    :param insulation_thickness: Insulation thickness in mm
    :return: EpcEfficiency
    """

    if insulation_thickness is None:
        raise ValueError("Insulation thickness is required for flat insulated efficiency calculation")

    if insulation_thickness >= 270:
        return EpcEfficiency.VERY_GOOD

    if 150 <= insulation_thickness <= 250:
        return EpcEfficiency.GOOD

    if 75 <= insulation_thickness <= 125:
        return EpcEfficiency.AVERAGE

    if 25 <= insulation_thickness <= 50:
        return EpcEfficiency.POOR

    return EpcEfficiency.VERY_POOR


def flat_efficiency(insulation_thickness: int | None, age_band: EpcConstructionAgeBand) -> EpcEfficiency:
    """
    Combines both age band and insulation thickness to determine flat roof efficiency.
    :param insulation_thickness: Insulation thickness in mm
    :param age_band: EpcConstructionAgeBand
    :return: EpcEfficiency
    """
    if insulation_thickness is not None:
        return flat_insulated_efficiency_thickness(insulation_thickness)

    return flat_insulated_efficiency_age_band(age_band)


def loft_insulated_efficiency(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
    """
    2023 onwards -> Very Good
    2012-2022 -> Very Good
    2007-2011 -> Very Good
    2003-2006 -> Very Good
    1996-2002 -> Good
    1991-1995 -> Good
    1983-1990 -> Average
    1976-1982 -> Average
    1967-1975 -> Average
    1950-1966 -> Average
    1930-1949 -> Average
    1900-1929 -> Average
    before 1900 -> Average
    :param age_band: Input age band, EpcConstructionAgeBand
    :return: EpcEfficiency
    """
    year = age_band.start_year()
    if year >= 2003:
        return EpcEfficiency.VERY_GOOD
    if year >= 1991:
        return EpcEfficiency.GOOD

    return EpcEfficiency.AVERAGE


def thatched_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
    """
    Maps thatched roof efficiency based on construction age band.
    :param age_band: EpcConstructionAgeBand
    :return: EpcEfficiency
    """
    year = age_band.start_year()
    if year >= 2023:
        return EpcEfficiency.VERY_GOOD
    if year >= 2003:
        return EpcEfficiency.GOOD

    return EpcEfficiency.AVERAGE


def thatched_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
    """
    Maps thatched roof efficiency based on insulation thickness.
    :param insulation_thickness: Insulation thickness in mm
    :return: EpcEfficiency
    """
    if insulation_thickness is None:
        raise ValueError("Insulation thickness is required for thatched efficiency calculation")

    if insulation_thickness >= 175:
        return EpcEfficiency.VERY_GOOD

    if insulation_thickness >= 25:
        return EpcEfficiency.GOOD

    return EpcEfficiency.AVERAGE


def thatched_efficiency(
    insulation_thickness: int | None,
    age_band: EpcConstructionAgeBand,
) -> EpcEfficiency:
    """
    Combines both age band and insulation thickness to determine thatched roof efficiency.
    :param insulation_thickness: Insulation thickness in mm
    :param age_band: EpcConstructionAgeBand
    :return: EpcEfficiency
    """
    if insulation_thickness is not None:
        return thatched_efficiency_thickness(insulation_thickness)

    return thatched_efficiency_age_band(age_band)


def sloping_ceiling_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
    """
    Maps sloping ceiling roof efficiency based on construction age band.
    :param age_band: EpcConstructionAgeBand
    :return: EpcEfficiency
    """
    year = age_band.start_year()
    if year >= 2023:
        return EpcEfficiency.VERY_GOOD
    if year >= 2003:
        return EpcEfficiency.GOOD
    if year >= 1983:
        return EpcEfficiency.AVERAGE
    if year >= 1976:
        return EpcEfficiency.POOR

    return EpcEfficiency.VERY_POOR


def sloping_ceiling_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
    """
    Maps sloping ceiling roof efficiency based on insulation thickness.
    :param insulation_thickness: Insulation thickness in mm
    :return: EpcEfficiency
    """
    if insulation_thickness is None:
        raise ValueError("Insulation thickness is required for sloping ceiling efficiency calculation")

    if insulation_thickness >= 270:
        return EpcEfficiency.VERY_GOOD

    if insulation_thickness >= 150:
        return EpcEfficiency.GOOD

    if insulation_thickness >= 75:
        return EpcEfficiency.AVERAGE

    if insulation_thickness >= 25:
        return EpcEfficiency.POOR

    return EpcEfficiency.VERY_POOR


def sloping_ceiling_efficiency(
    insulation_thickness: int | None,
    age_band: EpcConstructionAgeBand,
) -> EpcEfficiency:
    """
    Combines both age band and insulation thickness to determine sloping ceiling roof efficiency.
    :param insulation_thickness: Insulation thickness in mm
    :param age_band: EpcConstructionAgeBand
    :return: EpcEfficiency
    """
    if insulation_thickness is not None:
        return sloping_ceiling_efficiency_thickness(insulation_thickness)

    return sloping_ceiling_efficiency_age_band(age_band)


def loft_insulated_at_rafters_efficiency_thickness(insulation_thickness: int | None) -> EpcEfficiency:
    """
    400mm, 350mm = very good
    200-300mm = good
    125-175 = average
    50-100 = poor
    25 and below= very poor
    :return:
    """
    if insulation_thickness is None:
        raise ValueError("Insulation thickness is required for loft insulated at rafters efficiency calculation")

    if insulation_thickness >= 350:
        return EpcEfficiency.VERY_GOOD

    if insulation_thickness >= 200:
        return EpcEfficiency.GOOD

    if insulation_thickness >= 125:
        return EpcEfficiency.AVERAGE

    if insulation_thickness >= 50:
        return EpcEfficiency.POOR

    return EpcEfficiency.VERY_POOR


def loft_insulated_at_rafters_efficiency_age_band(age_band: EpcConstructionAgeBand) -> EpcEfficiency:
    """
    # 2023 onwards -> Very Good
    # 2003-2006, 2012-2022 -> Good
    # 1983 - 1990, 1996-2002 -> Average
    # 1976-1982 -> Poor
    # 1967-1975 and earlier bands -> Very Poor
    :param age_band: EpcConstructionAgeBand
    :return: EpcEfficiency
    """
    year = age_band.start_year()
    if year >= 2023:
        return EpcEfficiency.VERY_GOOD
    if year >= 2003:
        return EpcEfficiency.GOOD
    if year >= 1983:
        return EpcEfficiency.AVERAGE
    if year >= 1976:
        return EpcEfficiency.POOR

    return EpcEfficiency.VERY_POOR


def loft_insulated_at_rafters_efficiency(
    insulation_thickness: int | None,
    age_band: EpcConstructionAgeBand,
) -> EpcEfficiency:
    """
    Combines both age band and insulation thickness to determine loft insulated at rafters roof efficiency.
    :param insulation_thickness: Insulation thickness in mm
    :param age_band: EpcConstructionAgeBand
    :return: EpcEfficiency
    """
    if insulation_thickness is not None:
        return loft_insulated_at_rafters_efficiency_thickness(insulation_thickness)

    return loft_insulated_at_rafters_efficiency_age_band(age_band)


ROOF_DESCRIPTION_EFFICIENCIES: Mapping[EpcRoofDescriptions, RoofEfficiencyRule] = {
    # Flat roof
    EpcRoofDescriptions.flat_no_insulation: EpcEfficiency.VERY_POOR,
    EpcRoofDescriptions.flat_limited_insulation: flat_efficiency,
    EpcRoofDescriptions.flat_insulated: flat_efficiency,

    # Loft:
    # value mappings
    EpcRoofDescriptions.loft_12mm_insulation: EpcEfficiency.VERY_POOR,
    EpcRoofDescriptions.loft_25mm_insulation: EpcEfficiency.POOR,
    EpcRoofDescriptions.loft_50mm_insulation: EpcEfficiency.POOR,
    EpcRoofDescriptions.loft_75mm_insulation: EpcEfficiency.AVERAGE,
    EpcRoofDescriptions.loft_100mm_insulation: EpcEfficiency.AVERAGE,
    EpcRoofDescriptions.loft_125mm_insulation: EpcEfficiency.AVERAGE,
    EpcRoofDescriptions.loft_150mm_insulation: EpcEfficiency.GOOD,
    EpcRoofDescriptions.loft_175mm_insulation: EpcEfficiency.GOOD,
    EpcRoofDescriptions.loft_200mm_insulation: EpcEfficiency.GOOD,
    EpcRoofDescriptions.loft_250mm_insulation: EpcEfficiency.GOOD,
    EpcRoofDescriptions.loft_270mm_insulation: EpcEfficiency.VERY_GOOD,
    EpcRoofDescriptions.loft_300mm_insulation: EpcEfficiency.VERY_GOOD,
    EpcRoofDescriptions.loft_350mm_insulation: EpcEfficiency.VERY_GOOD,
    EpcRoofDescriptions.loft_400mm_plus_insulation: EpcEfficiency.VERY_GOOD,
    EpcRoofDescriptions.pitched_no_insulation: EpcEfficiency.VERY_POOR,
    # function mappings
    EpcRoofDescriptions.pitched_insulated_assumed: loft_insulated_efficiency,

    # Loft af rafters
    EpcRoofDescriptions.loft_insulated_at_rafters: loft_insulated_at_rafters_efficiency,

    # Another dwelling above
    EpcRoofDescriptions.another_dwelling_above: EpcEfficiency.NA,

    # Thatched
    EpcRoofDescriptions.thatched: thatched_efficiency,
    EpcRoofDescriptions.thatched_with_additional_insulation: thatched_efficiency,

    # Sloping ceiling
    EpcRoofDescriptions.sloping_pitched_insulated: sloping_ceiling_efficiency,
    EpcRoofDescriptions.sloping_pitched_limited_insulation: sloping_ceiling_efficiency,
    EpcRoofDescriptions.sloping_pitched_no_insulation: EpcEfficiency.VERY_POOR,

}


def resolve_roof_efficiency(
    description: EpcRoofDescriptions,
    age_band: EpcConstructionAgeBand | None,
    insulation_thickness: int | None,
) -> EpcEfficiency:
    """
    Resolve roof efficiency from description + age band + insulation thickness.
    """

    # Unknown / holding descriptions → efficiency unknown
    if description in description.unknown_descriptions:
        return EpcEfficiency.NA

    rule = ROOF_DESCRIPTION_EFFICIENCIES.get(description)

    if rule is None:
        return EpcEfficiency.NA

    # Fixed efficiency
    if isinstance(rule, EpcEfficiency):
        return rule

    # Callable rule
    if age_band is None or pd.isnull(age_band):
        return EpcEfficiency.NA

    try:
        # Try (thickness, age_band)
        return rule(insulation_thickness, age_band)
    except TypeError:
        # Fallback to (age_band)
        return rule(age_band)
