import hashlib
import json
from datetime import datetime
from typing import Literal

from pydantic import BaseModel

from inoopa_utils.custom_types import DecisionMakerDepartment
from inoopa_utils.custom_types.addresses import (
    Country,
    ProvinceBe,
    ProvinceFr,
    ProvinceNl,
    RegionBe,
    RegionFr,
    RegionNl,
)
from inoopa_utils.custom_types.legal_filters import (
    EmployeeCategoryClass,
    EntityType,
    LegalFormBe,
    LegalFormCategory,
    LegalFormNl,
)

MongoFilters = dict[str, dict[str, dict[str, int | str | list[str]]]]

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.northern_netherlands,
    ProvinceNl.friesland: RegionNl.northern_netherlands,
    ProvinceNl.drenthe: RegionNl.northern_netherlands,
    ProvinceNl.noord_holland: RegionNl.western_netherlands,
    ProvinceNl.zuid_holland: RegionNl.western_netherlands,
    ProvinceNl.utrecht: RegionNl.western_netherlands,
    ProvinceNl.zeeland: RegionNl.western_netherlands,
    ProvinceNl.noord_brabant: RegionNl.southern_netherlands,
    ProvinceNl.limburg: RegionNl.southern_netherlands,
    ProvinceNl.overijssel: RegionNl.eastern_netherlands,
    ProvinceNl.gelderland: RegionNl.eastern_netherlands,
    ProvinceNl.flevoland: RegionNl.eastern_netherlands,
    ProvinceNl.not_found: RegionNl.not_found,
}

def get_region_from_province(province: ProvinceBe | ProvinceNl | None, country: Country) -> RegionBe | RegionNl | 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


AdditionalFields = Literal[
    "email",
    "phone",
    "website",
    "nace_codes",
    "social_medias",
    "decision_makers_name",
    "decision_makers_email",
    "board_members",
]


class LeadGenerationCompanyFilters(BaseModel):
    """
    Represents the filters used to generate the leads.

    This is a pydantic model, so it can be used in the apis as a parameter.
    """

    additional_fields: list[AdditionalFields] | None = [
        "email",
        "phone",
        "website",
        "nace_codes",
        "social_medias",
        "decision_makers_name",
        "decision_makers_email",
        "board_members",
    ]

    countries: list[Country] | None = None
    regions: list[RegionBe | RegionFr | RegionNl] | None = None
    provinces: list[ProvinceBe | ProvinceFr | ProvinceNl] | None = None
    zipcodes: list[str] | None = None
    declared_nace_codes: list[str] | None = None
    declared_nace_codes_inclusive: list[str] | None = None
    best_nace_codes: list[str] | None = None
    best_nace_codes_inclusive: list[str] | None = None

    minimum_number_of_establishments: int | None = None
    maximum_number_of_establishments: int | None = None
    employee_categories: list[EmployeeCategoryClass] | None = None
    created_before: datetime | None = None
    created_after: datetime | None = None

    max_results: int | None = None
    max_decision_makers_per_company: int | None = None
    decision_makers_department_allowed: list[DecisionMakerDepartment] | None = None
    decision_makers_department_priority: list[DecisionMakerDepartment] | None = None
    excluded_companies: list[str] | None = None

    legal_form_Categories: list[LegalFormCategory] | None = None
    legal_forms: list[LegalFormBe | LegalFormNl] | None = None
    entity_types: list[EntityType] | None = None

    only_include_active_companies: bool = True
    only_include_companies_with_phone_no_do_not_call_me: bool = False
    only_include_companies_with_email: bool = False
    only_include_companies_with_website: bool = False
    only_include_companies_with_nace_codes: bool = False
    include_decision_makers_for_all_companies: bool = False

    def model_post_init(self, __context) -> None:
        # Filter out not_found sentinel values
        if self.regions:
            self.regions = [r for r in self.regions if r != RegionBe.not_found.value]
        if self.provinces:
            self.provinces = [p for p in self.provinces if p != ProvinceBe.not_found]
            # Only infer parent regions when no explicit regions were given.
            # When regions ARE specified, the $or in to_mongo_filters already
            # handles the combination (region OR province) correctly.
            if not self.regions:
                self.regions = []
                for province in self.provinces:
                    if province in PROVINCE_TO_REGION_MAPPER_BE and PROVINCE_TO_REGION_MAPPER_BE[province] not in self.regions:
                        self.regions.append(PROVINCE_TO_REGION_MAPPER_BE[province])
                    if province in PROVINCE_TO_REGION_MAPPER_NL and PROVINCE_TO_REGION_MAPPER_NL[province] not in self.regions:
                        self.regions.append(PROVINCE_TO_REGION_MAPPER_NL[province])

    def to_mongo_filters(self) -> MongoFilters:
        """
        Generate a MongoDB filter to search for companies based on the LeadGenerationCompanyFilters.

        can be used as a filter parameter in the MongoDB Collection.find() method.
        """
        filters = {}
        if self.countries:
            filters["country"] = {"$in": [c.value for c in self.countries]}
        if self.legal_forms:
            filters["legal_form"] = {"$in": [form.value for form in self.legal_forms]}
        if self.employee_categories:
            filters["employee_category_formatted"] = {"$in": [c.value for c in self.employee_categories]}
        if self.regions and self.provinces:
            # OR: companies in any of the specified regions OR in any of the specified provinces.
            # This correctly handles cross-region combinations such as
            # "Wallonia region + Vlaams-Brabant province" without losing either side.
            filters["$or"] = [
                {"address.region": {"$in": [r.value for r in self.regions]}},
                {"address.province": {"$in": [p.value for p in self.provinces]}},
            ]
        elif self.regions:
            filters["address.region"] = {"$in": [r.value for r in self.regions]}
        elif self.provinces:
            filters["address.province"] = {"$in": [p.value for p in self.provinces]}
        if self.zipcodes:

            zipcodes = [z if z != "NOT FOUND" else None for z in self.zipcodes]
            filters["address.postal_code"] = {"$in": zipcodes}

        if self.minimum_number_of_establishments and self.maximum_number_of_establishments:
            filters["number_of_establishments"] = {
                "$gte": self.minimum_number_of_establishments,
                "$lte": self.maximum_number_of_establishments,
            }
        elif self.minimum_number_of_establishments:
            filters["number_of_establishments"] = {"$gte": self.minimum_number_of_establishments}
        elif self.maximum_number_of_establishments:
            filters["number_of_establishments"] = {"$lte": self.maximum_number_of_establishments}

        if self.declared_nace_codes:
            filters["$or"] = [
                {"nace_codes": {"$elemMatch": {"number": {"$in": self.declared_nace_codes}}}},
                {
                    "establishments": {
                        "$elemMatch": {"nace_codes": {"$elemMatch": {"number": {"$in": self.declared_nace_codes}}}}
                    }
                },
            ]
        if self.declared_nace_codes_inclusive:
            nace_section_codes = []
            nace_codes_regex = []
            for code in self.declared_nace_codes_inclusive:
                # if the regex is only one letter, it's a nace section code
                if len(code) == 1:
                    # If the section code is not a letter, something went wrong, ignore it
                    if not code.isalpha():
                        continue
                    nace_section_codes.append(code.upper())
                else:
                    # if the regex is more than one letter, it's a nace code regex
                    nace_codes_regex.append(f"^{code}.*")

            filters["$or"] = []
            if nace_codes_regex:
                filters["$or"].append(
                    {"nace_codes": {"$elemMatch": {"number": {"$regex": "|".join(nace_codes_regex)}}}}
                )
                filters["$or"].append(
                    {
                        "establishments": {
                            "$elemMatch": {
                                "nace_codes": {"$elemMatch": {"number": {"$regex": "|".join(nace_codes_regex)}}}
                            }
                        }
                    },
                )
            if nace_section_codes:
                filters["$or"].append({"nace_codes": {"$elemMatch": {"section_code": {"$in": nace_section_codes}}}})
                filters["$or"].append(
                    {
                        "establishments": {
                            "$elemMatch": {"nace_codes": {"$elemMatch": {"section_code": {"$in": nace_section_codes}}}}
                        }
                    },
                )

        if self.best_nace_codes:
            filters["best_nace_codes.first_best_nace_code.number"] = {"$in": self.best_nace_codes}
        if self.best_nace_codes_inclusive:
            nace_codes = []
            nace_section_codes = []
            for code in self.best_nace_codes_inclusive:
                if len(code) == 1:
                    if not code.isalpha():
                        continue
                    nace_section_codes.append(code.upper())
                else:
                    # if the code is more than one letter, it's a nace code not a section
                    nace_codes.append(f"^{code}.*")
            filters["$or"] = []
            if nace_codes:
                filters["$or"].append({"best_nace_codes.first_best_nace_code.number": {"$regex": "|".join(nace_codes)}})
            if nace_section_codes:
                filters["$or"].append(
                    {"best_nace_codes.first_best_nace_code.section_code": {"$in": nace_section_codes}}
                )

        if self.legal_form_Categories:
            filters["legal_form_type"] = {"$in": [c.value for c in self.legal_form_Categories]}
        if self.entity_types:
            filters["entity_type"] = {"$in": [e.value for e in self.entity_types]}
        if self.created_before and not self.created_after:
            filters["start_date"] = {"$lte": self.created_before}
        if self.created_after and not self.created_before:
            filters["start_date"] = {"$gte": self.created_after}
        if self.created_before and self.created_after:
            filters["start_date"] = {"$lte": self.created_before, "$gte": self.created_after}
        if self.excluded_companies:
            filters["_id"] = {"$nin": self.excluded_companies}
        if self.only_include_active_companies:
            filters["status"] = "Active"
            filters["legal_situation"] = "Normal situation"

        if self.only_include_companies_with_phone_no_do_not_call_me:
            filters["best_phone"] = {"$ne": None}
            filters["best_phone.phone"] = {"$nin": ["DO_NOT_CALL_ME", None]}

        if self.only_include_companies_with_email:
            filters["best_email"] = {"$ne": None}
        if self.only_include_companies_with_website:
            filters["best_website"] = {"$ne": None}
        if self.only_include_companies_with_nace_codes:
            filters["best_nace_codes"] = {"$ne": None}
        return _optimize_query(filters)

    def to_dict(self) -> dict:
        data_dict = self.model_dump()
        # Convert datetime objects to isoformat for json serialization
        if self.created_before:
            data_dict["created_before"] = _datetime_serializer(self.created_before)
        if self.created_after:
            data_dict["created_after"] = _datetime_serializer(self.created_after)
        return data_dict

    def to_hash(self, semantic_search_query: str | None = None) -> str:
        """Build a deterministic cache key for these filters."""
        hashable_dict = {
            "minimum_number_of_establishments": self.minimum_number_of_establishments,
            "maximum_number_of_establishments": self.maximum_number_of_establishments,
            "max_results": self.max_results,
            "created_before": self.created_before.__str__() if self.created_before else None,
            "created_after": self.created_after.__str__() if self.created_after else None,
            "max_decision_makers_per_company": self.max_decision_makers_per_company,
            # Sort all lists to get a similar hash for similar requests
            "zipcodes": sorted(self.zipcodes) if self.zipcodes else None,
            "declared_nace_codes": sorted(self.declared_nace_codes) if self.declared_nace_codes else None,
            "declared_nace_codes_inclusive": sorted(self.declared_nace_codes_inclusive)
            if self.declared_nace_codes_inclusive
            else None,
            "best_nace_codes": sorted(self.best_nace_codes) if self.best_nace_codes else None,
            "best_nace_codes_inclusive": sorted(self.best_nace_codes_inclusive)
            if self.best_nace_codes_inclusive
            else None,
            "excluded_companies": sorted(self.excluded_companies) if self.excluded_companies else None,
            "countries": sorted([v.value for v in self.countries]) if self.countries else None,
            "employee_categories": sorted([v.value for v in self.employee_categories])
            if self.employee_categories
            else None,
            "legal_form_Categories": sorted([v.value for v in self.legal_form_Categories])
            if self.legal_form_Categories
            else None,
            "entity_types": sorted([v.value for v in self.entity_types]) if self.entity_types else None,
            "legal_forms": sorted([v.value for v in self.legal_forms]) if self.legal_forms else None,
            "regions": sorted([v.value for v in self.regions]) if self.regions else None,
            "provinces": sorted([v.value for v in self.provinces]) if self.provinces else None,
            "decision_makers_department_allowed": sorted([v.value for v in self.decision_makers_department_allowed])
            if self.decision_makers_department_allowed
            else None,
            # ! This one requires following the user's order
            "decision_makers_department_priority": [v.value for v in self.decision_makers_department_priority]
            if self.decision_makers_department_priority
            else None,
            "only_include_active_companies": self.only_include_active_companies,
            "only_include_companies_with_phone_no_do_not_call_me": self.only_include_companies_with_phone_no_do_not_call_me,
            "only_include_companies_with_email": self.only_include_companies_with_email,
            "only_include_companies_with_website": self.only_include_companies_with_website,
            "only_include_companies_with_nace_codes": self.only_include_companies_with_nace_codes,
            "include_decision_makers_for_all_companies": self.include_decision_makers_for_all_companies,
        }
        hashable_dict["semantic_search_query"] = semantic_search_query.strip().lower() if semantic_search_query else ""
        return _to_cache_key(hashable_dict)


class EnrichmentCompanyFilters(BaseModel):
    vats_to_enrich: list[str]

    only_include_active_companies: bool = True
    only_include_companies_with_phone_no_do_not_call_me: bool = False
    only_include_companies_with_email: bool = False
    only_include_companies_with_website: bool = False
    only_include_companies_with_nace_codes: bool = False
    include_decision_makers_for_all_companies: bool = False

    additional_fields: list[AdditionalFields] = [
        "email",
        "phone",
        "website",
        "nace_codes",
        "social_medias",
        "decision_makers_name",
        "decision_makers_email",
        "board_members",
    ]

    def to_mongo_filters(self) -> MongoFilters:
        filters: dict = {"_id": {"$in": self.vats_to_enrich}}
        if self.only_include_active_companies:
            filters["status"] = "Active"
            filters["legal_situation"] = "Normal situation"
        if self.only_include_companies_with_phone_no_do_not_call_me:
            filters["best_phone"] = {"$ne": None}
        if self.only_include_companies_with_email:
            filters["best_email"] = {"$ne": None}
        if self.only_include_companies_with_website:
            filters["best_website"] = {"$ne": None}
        if self.only_include_companies_with_nace_codes:
            filters["best_nace_codes"] = {"$ne": None}
        return _optimize_query(filters)

    def to_dict(self) -> dict:
        return self.model_dump()

    def to_hash(self) -> str:
        """Build a deterministic cache key for these filters."""
        hashable_dict = self.model_dump()
        hashable_dict["vats_to_enrich"] = sorted(set(self.vats_to_enrich))
        hashable_dict["additional_fields"] = sorted(set(self.additional_fields))
        return _to_cache_key(hashable_dict)


def _to_cache_key(value: dict) -> str:
    canonical_json = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str)
    digest = hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()
    return f"v2_{digest}"


def _datetime_serializer(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError(f"Type {type(obj)} not serializable")


def _optimize_query(filters: dict) -> dict:
    """
    Recursively optimize MongoDB filters by simplifying single-element $in/$nin/$or operators.

    Transformations:
    - {"$in": [single_value]} → single_value (direct equality)
    - {"$nin": [single_value]} → {"$ne": single_value}
    - {"$or": [single_condition]} → single_condition (inlined into parent)

    This preserves the structure of complex queries with $or, $elemMatch, etc.
    """
    if not filters:
        return filters

    result = {}
    for key, value in filters.items():
        if isinstance(value, dict):
            # Check for single-element $in with only $in key
            if "$in" in value and len(value) == 1 and isinstance(value["$in"], list) and len(value["$in"]) == 1:
                result[key] = value["$in"][0]
            # Check for single-element $nin with only $nin key
            elif "$nin" in value and len(value) == 1 and isinstance(value["$nin"], list) and len(value["$nin"]) == 1:
                result[key] = {"$ne": value["$nin"][0]}
            else:
                # Recursively process nested dicts
                result[key] = _optimize_query(value)
        elif isinstance(value, list):
            # Single-element $or: inline the condition into the parent dict
            if key == "$or" and len(value) == 1 and isinstance(value[0], dict):
                optimized = _optimize_query(value[0])
                result.update(optimized)
            else:
                # Recursively process lists (for $or arrays, etc.)
                result[key] = [_optimize_query(item) if isinstance(item, dict) else item for item in value]
        else:
            result[key] = value

    return result
