import xml.etree.ElementTree as ET
from typing import Any, List, Optional, TypedDict


from backend.ecmk_fetcher.reports import build_property_id
from datatypes.epc.domain.field_mappings import PROPERTY_TYPE_LOOKUP


# This file should ultimately live somewhere different, probably
class Floor(TypedDict):
    area_m2: float
    height_m: float
    heat_loss_perimeter_m: float
    party_wall_length_m: float


class Roof(TypedDict, total=False):
    construction: int  # TODO: map to str
    insulation_location: int | str  # TODO: map to str
    insulation_thickness_mm: float | str


class BuildingPart(TypedDict):
    identifier: str  # e.g. "Main Dwelling", "Extension"
    floors: List[Floor]
    roof: Optional[Roof]


class SapPropertyDetails(TypedDict):
    reference: str
    address: str
    property_type: str
    building_parts: List[BuildingPart]


def _get_namespace(tag: str) -> str:
    return tag.split("}")[0].strip("{")


def _require_text(value: Optional[str], field: str) -> str:
    if value is None:
        raise ValueError(f"Missing required field: {field}")
    return value


def _parse_float(value: Optional[str], field: str) -> float:
    if value is None:
        raise ValueError(f"Missing float field: {field}")
    return float(value)


def _parse_int(value: Optional[str], field: str) -> int:
    if value is None:
        raise ValueError(f"Missing int field: {field}")
    return int(value)


def _parse_thickness_mm(value: Optional[str]) -> Optional[float | str]:
    if value is None:
        return None
    stripped = value.replace("mm", "").strip()
    try:
        return float(stripped)
    except ValueError:
        return stripped


def parse_rdsap(xml_string: str) -> SapPropertyDetails:
    root = ET.fromstring(xml_string)

    ns_uri: str = _get_namespace(root.tag)
    ns: dict[str, str] = {"r": ns_uri}

    # --- Address ---
    addr_elem = root.find(".//r:Address", ns)
    if addr_elem is None:
        raise ValueError("Address element not found")

    address_line_1: str = addr_elem.findtext(
        "r:Address-Line-1", default="", namespaces=ns
    )
    postcode: str = addr_elem.findtext("r:Postcode", default="", namespaces=ns)

    address_parts: List[str] = [
        address_line_1,
        addr_elem.findtext("r:Address-Line-2", default="", namespaces=ns),
        addr_elem.findtext("r:Post-Town", default="", namespaces=ns),
        postcode,
    ]

    address: str = ", ".join(part for part in address_parts if part)
    reference: str = build_property_id(address_line_1, postcode)

    # --- Property Type ---
    prop_type_text = root.findtext(".//r:Property-Type", namespaces=ns)
    prop_type_code: int = _parse_int(prop_type_text, "Property-Type")
    property_type: str = PROPERTY_TYPE_LOOKUP[prop_type_code]

    # --- Building Parts ---
    building_parts: List[BuildingPart] = []

    for bp in root.findall(".//r:SAP-Building-Part", ns):

        identifier_text = bp.findtext("r:Identifier", namespaces=ns)
        identifier: str = _require_text(identifier_text, "Identifier")

        # Floors
        floors: List[Floor] = []

        for f in bp.findall(".//r:SAP-Floor-Dimension", ns):

            area = _parse_float(
                f.findtext("r:Total-Floor-Area", namespaces=ns),
                "Total-Floor-Area",
            )

            height = _parse_float(
                f.findtext("r:Room-Height", namespaces=ns),
                "Room-Height",
            )

            heat_loss = _parse_float(
                f.findtext("r:Heat-Loss-Perimeter", namespaces=ns),
                "Heat-Loss-Perimeter",
            )

            party_wall = _parse_float(
                f.findtext("r:Party-Wall-Length", namespaces=ns),
                "Party-Wall-Length",
            )

            floor: Floor = {
                "area_m2": area,
                "height_m": height,
                "heat_loss_perimeter_m": heat_loss,
                "party_wall_length_m": party_wall,
            }

            floors.append(floor)

        # Roof (optional)
        roof: Optional[Roof] = None

        roof_construction_text = bp.findtext("r:Roof-Construction", namespaces=ns)
        roof_ins_loc_text = bp.findtext("r:Roof-Insulation-Location", namespaces=ns)
        roof_thickness_text = bp.findtext("r:Roof-Insulation-Thickness", namespaces=ns)

        if (
            roof_construction_text is not None
            or roof_ins_loc_text is not None
            or roof_thickness_text is not None
        ):
            roof_dict: Roof = {}

            if roof_construction_text is not None:
                roof_dict["construction"] = _parse_int(
                    roof_construction_text, "Roof-Construction"
                )

            if roof_ins_loc_text is not None:
                try:
                    roof_dict["insulation_location"] = _parse_int(
                        roof_ins_loc_text, "Roof-Insulation-Location"
                    )
                except ValueError:
                    roof_dict["insulation_location"] = roof_ins_loc_text

            thickness = _parse_thickness_mm(roof_thickness_text)
            if thickness is not None:
                roof_dict["insulation_thickness_mm"] = thickness

            roof = roof_dict

        building_part: BuildingPart = {
            "identifier": identifier,
            "floors": floors,
            "roof": roof,
        }

        building_parts.append(building_part)

    result: SapPropertyDetails = {
        "reference": reference,
        "address": address,
        "property_type": property_type,
        "building_parts": building_parts,
    }

    return result


def _normalise_identifier(identifier: str) -> str:
    return identifier.lower().replace(" ", "_").replace("-", "_")


def flatten_sap_property(details: SapPropertyDetails) -> dict[str, Any]:
    row: dict[str, Any] = {}

    row["reference"] = details["reference"]
    row["address"] = details["address"]
    row["property_type"] = details["property_type"]

    for bp in details["building_parts"]:
        prefix = _normalise_identifier(bp["identifier"])

        for i, floor in enumerate(bp["floors"], start=1):
            floor_prefix = f"{prefix}_floor_{i}"
            row[f"{floor_prefix}_area_m2"] = floor["area_m2"]
            row[f"{floor_prefix}_height_m"] = floor["height_m"]
            row[f"{floor_prefix}_heat_loss_perimeter_m"] = floor[
                "heat_loss_perimeter_m"
            ]
            row[f"{floor_prefix}_party_wall_length_m"] = floor["party_wall_length_m"]

        roof = bp.get("roof")
        if roof:
            if "construction" in roof:
                row[f"{prefix}_roof_construction"] = roof["construction"]
            if "insulation_location" in roof:
                row[f"{prefix}_roof_insulation_location"] = roof["insulation_location"]
            if "insulation_thickness_mm" in roof:
                row[f"{prefix}_roof_insulation_thickness_mm"] = roof[
                    "insulation_thickness_mm"
                ]

    return row
