from enum import Enum


class ProvinceBe(Enum):
    antwerpen = "Antwerpen (BE)"
    vlaams_brabant = "Vlaams-Brabant (BE)"
    brabant_wallon = "Brabant-Wallon (BE)"
    bruxelles = "Bruxelles (BE)"
    west_vlaanderen = "West-Vlaanderen (BE)"
    oost_vlaanderen = "Oost-Vlaanderen (BE)"
    hainaut = "Hainaut (BE)"
    limbourg = "Limbourg (BE)"
    liege = "Liege (BE)"
    luxembourg = "Luxembourg (BE)"
    namur = "Namur (BE)"
    not_found = "NOT DECLARED"

    @classmethod
    def get_all_values(cls):
        """Return all possible values for this enum as a list of enum."""
        return [v for v in cls.__dict__.values() if isinstance(v, cls)]

    @classmethod
    def get_all_values_str(cls) -> list[str]:
        """Return all possible values for this enum as a list of str."""
        return [v.value for v in cls.__dict__.values() if isinstance(v, cls)]


class ProvinceFr(Enum):
    @classmethod
    def __new__(cls, value):
        raise NotImplementedError("FR provinces not implemented yet")

    @classmethod
    def get_all_values(cls):
        """Return all possible values for this enum as a list of enum."""
        return [v for v in cls.__dict__.values() if isinstance(v, cls)]

    @classmethod
    def get_all_values_str(cls) -> list[str]:
        """Return all possible values for this enum as a list of str."""
        return [v.value for v in cls.__dict__.values() if isinstance(v, cls)]


class ProvinceNl(Enum):
    noord_holland = "Noord-Holland (NL)"
    zuid_holland = "Zuid-Holland (NL)"
    utrecht = "Utrecht (NL)"
    zeeland = "Zeeland (NL)"
    noord_brabant = "Noord-Brabant (NL)"
    limburg = "Limburg (NL)"
    gelderland = "Gelderland (NL)"
    overijssel = "Overijssel (NL)"
    drenthe = "Drenthe (NL)"
    flevoland = "Flevoland (NL)"
    friesland = "Friesland (NL)"
    groningen = "Groningen (NL)"
    not_found = "NOT DECLARED"

    @classmethod
    def get_all_values(cls):
        """Return all possible values for this enum as a list of enum."""
        return [v for v in cls.__dict__.values() if isinstance(v, cls)]

    @classmethod
    def get_all_values_str(cls) -> list[str]:
        """Return all possible values for this enum as a list of str."""
        return [v.value for v in cls.__dict__.values() if isinstance(v, cls)]


class RegionBe(Enum):
    wallonia = "Wallonie (BE)"
    flanders = "Vlaanderen (BE)"
    bruxelles = "Bruxelles (BE)"
    not_found = "NOT DECLARED"

    @classmethod
    def get_all_values(cls):
        """Return all possible values for this enum as a list of enum."""
        return [v for v in cls.__dict__.values() if isinstance(v, cls)]

    @classmethod
    def get_all_values_str(cls) -> list[str]:
        """Return all possible values for this enum as a list of str."""
        return [v.value for v in cls.__dict__.values() if isinstance(v, cls)]


class RegionFr(Enum):
    @classmethod
    def __new__(cls, value):
        raise NotImplementedError("FR regions not implemented yet")

    @classmethod
    def get_all_values(cls):
        """Return all possible values for this enum as a list of enum."""
        return [v for v in cls.__dict__.values() if isinstance(v, cls)]

    @classmethod
    def get_all_values_str(cls) -> list[str]:
        """Return all possible values for this enum as a list of str."""
        return [v.value for v in cls.__dict__.values() if isinstance(v, cls)]


class RegionNl(Enum):
    north_netherlands = "North Netherlands (NL)"
    east_netherlands = "East Netherlands (NL)"
    west_netherlands = "West Netherlands (NL)"
    south_netherlands = "South Netherlands (NL)"
    not_found = "NOT DECLARED"

    @classmethod
    def get_all_values(cls):
        """Return all possible values for this enum as a list of enum."""
        return [v for v in cls.__dict__.values() if isinstance(v, cls)]

    @classmethod
    def get_all_values_str(cls) -> list[str]:
        """Return all possible values for this enum as a list of str."""
        return [v.value for v in cls.__dict__.values() if isinstance(v, cls)]


class Country(Enum):
    belgium = "BE"
    france = "FR"
    netherlands = "NL"

    @classmethod
    def get_all_values(cls):
        """Return all possible values for this enum as a list of enum."""
        return [v for v in cls.__dict__.values() if isinstance(v, cls)]

    @classmethod
    def get_all_values_str(cls) -> list[str]:
        """Return all possible values for this enum as a list of str."""
        return [v.value for v in cls.__dict__.values() if isinstance(v, cls)]


POSTAL_CODE_TO_PROVINCE_MAPPER_BE = [
    (1000, 1299, ProvinceBe.bruxelles),
    (1300, 1499, ProvinceBe.brabant_wallon),
    (1500, 1999, ProvinceBe.vlaams_brabant),
    (2000, 2999, ProvinceBe.antwerpen),
    (3000, 3499, ProvinceBe.vlaams_brabant),
    (3500, 3999, ProvinceBe.limbourg),
    (4000, 4999, ProvinceBe.liege),
    (5000, 5999, ProvinceBe.namur),
    (6000, 6599, ProvinceBe.hainaut),
    (6600, 6999, ProvinceBe.luxembourg),
    (7000, 7999, ProvinceBe.hainaut),
    (8000, 8999, ProvinceBe.west_vlaanderen),
    (9000, 9999, ProvinceBe.oost_vlaanderen),
]


POSTAL_CODE_TO_PROVINCE_MAPPER_NL = [
    # Noord-Holland
    (1000, 1299, ProvinceNl.noord_holland),
    (1380, 1384, ProvinceNl.noord_holland),
    (1394, 1394, ProvinceNl.noord_holland),
    (1398, 1425, ProvinceNl.noord_holland),
    (1430, 2158, ProvinceNl.noord_holland),
    (2165, 2165, ProvinceNl.noord_holland),

    # Flevoland
    (1300, 1379, ProvinceNl.flevoland),
    (3890, 3899, ProvinceNl.flevoland),
    (8200, 8259, ProvinceNl.flevoland),
    (8300, 8322, ProvinceNl.flevoland),

    # Utrecht
    (1390, 1393, ProvinceNl.utrecht),
    (1396, 1396, ProvinceNl.utrecht),
    (1426, 1427, ProvinceNl.utrecht),
    (3382, 3464, ProvinceNl.utrecht),
    (3467, 3769, ProvinceNl.utrecht),
    (3795, 3836, ProvinceNl.utrecht),
    (3900, 3924, ProvinceNl.utrecht),
    (3926, 3999, ProvinceNl.utrecht),
    (4120, 4146, ProvinceNl.utrecht),
    (4163, 4169, ProvinceNl.utrecht),
    (4230, 4239, ProvinceNl.utrecht),
    (4242, 4249, ProvinceNl.utrecht),

    # Zuid-Holland
    (1428, 1429, ProvinceNl.zuid_holland),
    (2159, 2164, ProvinceNl.zuid_holland),
    (2166, 3381, ProvinceNl.zuid_holland),
    (3465, 3466, ProvinceNl.zuid_holland),
    (4200, 4209, ProvinceNl.zuid_holland),
    (4213, 4213, ProvinceNl.zuid_holland),
    (4220, 4229, ProvinceNl.zuid_holland),
    (4240, 4241, ProvinceNl.zuid_holland),

    # Gelderland
    (3770, 3794, ProvinceNl.gelderland),
    (3837, 3888, ProvinceNl.gelderland),
    (3925, 3925, ProvinceNl.gelderland),
    (4000, 4119, ProvinceNl.gelderland),
    (4147, 4162, ProvinceNl.gelderland),
    (4170, 4199, ProvinceNl.gelderland),
    (4211, 4212, ProvinceNl.gelderland),
    (4214, 4219, ProvinceNl.gelderland),
    (5300, 5335, ProvinceNl.gelderland),
    (6500, 6583, ProvinceNl.gelderland),
    (6600, 7399, ProvinceNl.gelderland),
    (7439, 7439, ProvinceNl.gelderland),
    (8050, 8054, ProvinceNl.gelderland),
    (8070, 8099, ProvinceNl.gelderland),
    (8160, 8195, ProvinceNl.gelderland),

    # Zeeland
    (4300, 4599, ProvinceNl.zeeland),
    (4672, 4679, ProvinceNl.zeeland),
    (4682, 4699, ProvinceNl.zeeland),

    # Noord-Brabant
    (4250, 4299, ProvinceNl.noord_brabant),
    (4600, 4671, ProvinceNl.noord_brabant),
    (4680, 4681, ProvinceNl.noord_brabant),
    (4700, 4944, ProvinceNl.noord_brabant),
    (5000, 5299, ProvinceNl.noord_brabant),
    (5340, 5765, ProvinceNl.noord_brabant),
    (5820, 5846, ProvinceNl.noord_brabant),
    (6020, 6029, ProvinceNl.noord_brabant),

    # Limburg
    (5766, 5817, ProvinceNl.limburg),
    (5850, 5995, ProvinceNl.limburg),
    (6000, 6019, ProvinceNl.limburg),
    (6030, 6499, ProvinceNl.limburg),
    (6584, 6599, ProvinceNl.limburg),

    # Overijssel
    (7400, 7438, ProvinceNl.overijssel),
    (7440, 7739, ProvinceNl.overijssel),
    (7767, 7799, ProvinceNl.overijssel),
    (7950, 7955, ProvinceNl.overijssel),
    (8000, 8049, ProvinceNl.overijssel),
    (8055, 8069, ProvinceNl.overijssel),
    (8100, 8159, ProvinceNl.overijssel),
    (8196, 8199, ProvinceNl.overijssel),
    (8260, 8299, ProvinceNl.overijssel),
    (8323, 8349, ProvinceNl.overijssel),
    (8355, 8379, ProvinceNl.overijssel),

    # Drenthe
    (7740, 7766, ProvinceNl.drenthe),
    (7800, 7949, ProvinceNl.drenthe),
    (7956, 7991, ProvinceNl.drenthe),
    (8350, 8354, ProvinceNl.drenthe),
    (8380, 8387, ProvinceNl.drenthe),
    (9300, 9349, ProvinceNl.drenthe),
    (9400, 9478, ProvinceNl.drenthe),
    (9480, 9499, ProvinceNl.drenthe),
    (9510, 9539, ProvinceNl.drenthe),
    (9564, 9564, ProvinceNl.drenthe),
    (9570, 9579, ProvinceNl.drenthe),
    (9654, 9659, ProvinceNl.drenthe),
    (9749, 9749, ProvinceNl.drenthe),
    (9760, 9769, ProvinceNl.drenthe),

    # Friesland
    (8388, 8941, ProvinceNl.friesland),
    (9000, 9299, ProvinceNl.friesland),
    (9850, 9859, ProvinceNl.friesland),
    (9870, 9879, ProvinceNl.friesland),

    # Groningen
    (9350, 9399, ProvinceNl.groningen),
    (9479, 9479, ProvinceNl.groningen),
    (9500, 9509, ProvinceNl.groningen),
    (9540, 9563, ProvinceNl.groningen),
    (9565, 9569, ProvinceNl.groningen),
    (9580, 9653, ProvinceNl.groningen),
    (9660, 9748, ProvinceNl.groningen),
    (9750, 9759, ProvinceNl.groningen),
    (9770, 9849, ProvinceNl.groningen),
    (9860, 9869, ProvinceNl.groningen),
    (9880, 9999, ProvinceNl.groningen),
]


Region = RegionBe | RegionFr | RegionNl
Province = ProvinceBe | ProvinceFr | ProvinceNl


def get_province_from_postal_code(postal_code: str | None, country: Country) -> Province | None:
    """Get the province from a postal code for the given country.

    Sources:
    - BE: https://www.spotzi.com/en/data-catalog/categories/postal-codes/belgium/
    - NL: https://nl.wikipedia.org/wiki/Postcodes_in_Nederland
    """
    if not postal_code:
        return None

    if country == Country.belgium:
        try:
            postal_code_int = int(postal_code.strip())
        except ValueError:
            return None
        for postal_start, postal_stop, province in POSTAL_CODE_TO_PROVINCE_MAPPER_NL:
            if postal_start <= postal_code_int <= postal_stop:
                return province
        return None

    elif country == Country.netherlands:
        try:
            postal_prefix = int(postal_code[:4])
        except (ValueError, TypeError):
            return None
        for postal_start, postal_stop, province in POSTAL_CODE_TO_PROVINCE_MAPPER_NL:
            if postal_start <= postal_prefix <= postal_stop:
                return province
        return None

    else:
        raise NotImplementedError(f"Country {country.value} not implemented yet.")


PROVINCE_TO_REGION_MAPPER_BE = {
    ProvinceBe.antwerpen: RegionBe.flanders,
    ProvinceBe.limbourg: RegionBe.flanders,
    ProvinceBe.oost_vlaanderen: RegionBe.flanders,
    ProvinceBe.west_vlaanderen: RegionBe.flanders,
    ProvinceBe.vlaams_brabant: RegionBe.flanders,
    ProvinceBe.bruxelles: RegionBe.bruxelles,
    ProvinceBe.brabant_wallon: RegionBe.wallonia,
    ProvinceBe.hainaut: RegionBe.wallonia,
    ProvinceBe.liege: RegionBe.wallonia,
    ProvinceBe.namur: RegionBe.wallonia,
    ProvinceBe.luxembourg: RegionBe.wallonia,
}

PROVINCE_TO_REGION_MAPPER_NL = {
    ProvinceNl.groningen: RegionNl.north_netherlands,
    ProvinceNl.friesland: RegionNl.north_netherlands,
    ProvinceNl.drenthe: RegionNl.north_netherlands,

    ProvinceNl.overijssel: RegionNl.east_netherlands,
    ProvinceNl.gelderland: RegionNl.east_netherlands,
    ProvinceNl.flevoland: RegionNl.east_netherlands,

    ProvinceNl.utrecht: RegionNl.west_netherlands,
    ProvinceNl.noord_holland: RegionNl.west_netherlands,
    ProvinceNl.zuid_holland: RegionNl.west_netherlands,
    ProvinceNl.zeeland: RegionNl.west_netherlands,

    ProvinceNl.noord_brabant: RegionNl.south_netherlands,
    ProvinceNl.limburg: RegionNl.south_netherlands,
}


def get_region_from_province(province: Province | None, country: Country) -> Region | None:
    """Get the region from the province for the given country."""
    if not province:
        return None

    if country == Country.belgium:
        assert isinstance(province, ProvinceBe)
        return PROVINCE_TO_REGION_MAPPER_BE.get(province)

    if country == Country.netherlands:
        assert isinstance(province, ProvinceNl)
        return PROVINCE_TO_REGION_MAPPER_NL.get(province)

    return None


def get_all_regions(countries: list[Country] | list[str]) -> list[Region]:
    """Get all regions for a list of countries. Countires can be a list of Country enums or a list of str."""
    all_regions = []
    if not countries:
        return all_regions

    if isinstance(countries[0], str):
        countries = [Country(country) for country in countries]

    if Country.belgium in countries:
        all_regions.extend(RegionBe.get_all_values())
    if Country.france in countries:
        all_regions.extend(RegionFr.get_all_values())
    if Country.netherlands in countries:
        all_regions.extend(RegionNl.get_all_values())
    return all_regions


def get_all_regions_str(countries: list[Country] | list[str]) -> list[str]:
    """Get all regions for a list of countries. Countries can be a list of Country enums or a list of str."""
    all_regions = []
    if not countries:
        return all_regions

    if isinstance(countries[0], str):
        countries = [Country(country) for country in countries]

    for country in countries:
        if country == Country.belgium:
            all_regions.extend(RegionBe.get_all_values_str())
        elif country == Country.france:
            all_regions.extend(RegionFr.get_all_values_str())
        elif country == Country.netherlands:
            all_regions.extend(RegionNl.get_all_values_str())
        else:
            raise ValueError(f"Unknown country: `{country}`")

    return all_regions


def get_all_provinces(countries: list[Country] | list[str]) -> list[Province]:
    """Get all provinces for a list of countries. Countries can be a list of Country enums or a list of str."""
    all_provinces = []
    if not countries:
        return all_provinces

    if isinstance(countries[0], str):
        countries = [Country(country) for country in countries]

    if Country.belgium in countries:
        all_provinces.extend(ProvinceBe.get_all_values())
    if Country.france in countries:
        all_provinces.extend(ProvinceFr.get_all_values())
    if Country.netherlands in countries:
        all_provinces.extend(ProvinceNl.get_all_values())

    return all_provinces


def get_all_provinces_str(countries: list[Country] | list[str]) -> list[str]:
    """Get all provinces for a list of countries. Countries can be a list of Country enums or a list of str."""
    all_provinces = []
    if not countries:
        return all_provinces

    if isinstance(countries[0], str):
        countries = [Country(country) for country in countries]

    for country in countries:
        if country == Country.belgium:
            all_provinces.extend(ProvinceBe.get_all_values_str())
        elif country == Country.france:
            all_provinces.extend(ProvinceFr.get_all_values_str())
        elif country == Country.netherlands:
            all_provinces.extend(ProvinceNl.get_all_values_str())
        else:
            raise ValueError(f"Unknown country: `{country}`")

    return all_provinces


def coutries_to_enums(coutry_list: list[str]) -> list[Country]:
    """Format a list of countries as a string to a list of Country enums."""
    return [Country(country) for country in coutry_list]


def provinces_to_enums(province_list: list[str]) -> list[Province]:
    """Format a list of provinces as a string to a list of Province enums."""
    new_list = []
    for province in province_list:
        if province in ProvinceBe.get_all_values_str():
            province_enum = ProvinceBe(province)
        elif province in ProvinceFr.get_all_values_str():
            province_enum = ProvinceFr(province)
        elif province in ProvinceNl.get_all_values_str():
            province_enum = ProvinceNl(province)
        else:
            raise ValueError(f"Unknown province: `{province}`")
        new_list.append(province_enum)
    return new_list


def regions_to_enums(region_list: list[str]) -> list[Region]:
    """Format a list of regions as a string to a list of Region enums."""
    new_list = []
    for region in region_list:
        if region in RegionBe.get_all_values_str():
            region_enum = RegionBe(region)
        elif region in RegionFr.get_all_values_str():
            region_enum = RegionFr(region)
        elif region in RegionNl.get_all_values_str():
            region_enum = RegionNl(region)
        else:
            raise ValueError(f"Unknown region: `{region}`")
        new_list.append(region_enum)
    return new_list
