import xml.etree.ElementTree as ET
from uuid import uuid4

from epiuclid.serializers.pathway_mapper import IUCLIDEndpointStudyRecordData, SoilPropertiesData

from .base import (
    NS_PLATFORM_FIELDS,
    _sub,
    _tag,
    build_document,
    document_key,
)

NS_ESR_BIODEG = (
    "http://iuclid6.echa.europa.eu/namespaces/ENDPOINT_STUDY_RECORD-BiodegradationInSoil/10.0"
)
ET.register_namespace("", NS_ESR_BIODEG)

DOC_SUBTYPE = "BiodegradationInSoil"
PICKLIST_OTHER_CODE = "1342"
SOIL_TYPE_CODE_BY_KEY = {
    "CLAY": "257",
    "CLAY_LOAM": "258",
    "LOAM": "1026",
    "LOAMY_SAND": "1027",
    "SAND": "1522",
    "SANDY_CLAY_LOAM": "1523",
    "SANDY_LOAM": "1524",
    "SANDY_CLAY": "1525",
    "SILT": "1549",
    "SILT_LOAM": "1550",
    "SILTY_CLAY": "1551",
    "SILTY_CLAY_LOAM": "1552",
}
SOIL_CLASSIFICATION_CODE_BY_KEY = {
    "USDA": "1649",
    "DE": "314",
    "INTERNATIONAL": "1658",
}


class EndpointStudyRecordBuilder:
    def build(self, data: IUCLIDEndpointStudyRecordData) -> str:
        esr = ET.Element(f"{{{NS_ESR_BIODEG}}}ENDPOINT_STUDY_RECORD.{DOC_SUBTYPE}")

        soil_entries = list(data.soil_properties_entries)
        if not soil_entries and data.soil_properties is not None:
            soil_entries = [data.soil_properties]

        has_materials = bool(
            data.model_name_and_version
            or data.software_name_and_version
            or data.model_remarks
            or soil_entries
        )
        if has_materials:
            materials = _sub(esr, NS_ESR_BIODEG, "MaterialsAndMethods")

            if soil_entries:
                self._build_soil_structured_full(materials, soil_entries)

            if data.model_name_and_version or data.software_name_and_version or data.model_remarks:
                model_info = _sub(materials, NS_ESR_BIODEG, "ModelAndSoftware")

                for model_name in data.model_name_and_version:
                    _sub(model_info, NS_ESR_BIODEG, "ModelNameAndVersion", model_name)

                for software_name in data.software_name_and_version:
                    _sub(model_info, NS_ESR_BIODEG, "SoftwareNameAndVersion", software_name)

                for remark in data.model_remarks:
                    _sub(model_info, NS_ESR_BIODEG, "Remarks", remark)

        has_results = (
            data.half_lives or data.transformation_products or data.temperature is not None
        )
        if has_results:
            results = _sub(esr, NS_ESR_BIODEG, "ResultsAndDiscussion")

            if data.half_lives or data.temperature is not None:
                dt_parent = _sub(results, NS_ESR_BIODEG, "DTParentCompound")

                if data.half_lives:
                    for hl in data.half_lives:
                        entry = ET.SubElement(dt_parent, _tag(NS_ESR_BIODEG, "entry"))
                        entry.set(_tag(NS_PLATFORM_FIELDS, "uuid"), str(uuid4()))

                        if hl.soil_no_code:
                            soil_no = _sub(entry, NS_ESR_BIODEG, "SoilNo")
                            _sub(soil_no, NS_ESR_BIODEG, "value", hl.soil_no_code)

                        value_range = _sub(entry, NS_ESR_BIODEG, "Value")
                        _sub(value_range, NS_ESR_BIODEG, "unitCode", "2329")  # days
                        _sub(value_range, NS_ESR_BIODEG, "lowerValue", str(hl.dt50_start))
                        _sub(value_range, NS_ESR_BIODEG, "upperValue", str(hl.dt50_end))

                        temperature = (
                            hl.temperature if hl.temperature is not None else data.temperature
                        )
                        if temperature is not None:
                            temp_range = _sub(entry, NS_ESR_BIODEG, "Temp")
                            _sub(temp_range, NS_ESR_BIODEG, "unitCode", "2493")  # degree Celsius
                            _sub(temp_range, NS_ESR_BIODEG, "lowerValue", str(temperature[0]))
                            _sub(temp_range, NS_ESR_BIODEG, "upperValue", str(temperature[1]))

                        _sub(entry, NS_ESR_BIODEG, "KineticParameters", hl.model)
                else:
                    # Temperature without half-lives: single entry with only Temp
                    assert data.temperature is not None
                    entry = ET.SubElement(dt_parent, _tag(NS_ESR_BIODEG, "entry"))
                    entry.set(_tag(NS_PLATFORM_FIELDS, "uuid"), str(uuid4()))
                    temp_range = _sub(entry, NS_ESR_BIODEG, "Temp")
                    _sub(temp_range, NS_ESR_BIODEG, "unitCode", "2493")  # degree Celsius
                    _sub(temp_range, NS_ESR_BIODEG, "lowerValue", str(data.temperature[0]))
                    _sub(temp_range, NS_ESR_BIODEG, "upperValue", str(data.temperature[1]))

            if data.transformation_products:
                tp_details = _sub(results, NS_ESR_BIODEG, "TransformationProductsDetails")
                for tp in data.transformation_products:
                    entry = ET.SubElement(tp_details, _tag(NS_ESR_BIODEG, "entry"))
                    entry.set(_tag(NS_PLATFORM_FIELDS, "uuid"), str(tp.uuid))

                    _sub(
                        entry,
                        NS_ESR_BIODEG,
                        "IdentityOfCompound",
                        document_key(tp.product_reference_uuid),
                    )

                    if tp.parent_reference_uuids:
                        parents = _sub(entry, NS_ESR_BIODEG, "ParentCompoundS")
                        for parent_uuid in tp.parent_reference_uuids:
                            _sub(parents, NS_PLATFORM_FIELDS, "key", document_key(parent_uuid))

                    if tp.kinetic_formation_fraction is not None:
                        _sub(
                            entry,
                            NS_ESR_BIODEG,
                            "KineticFormationFraction",
                            str(tp.kinetic_formation_fraction),
                        )

        doc_key = document_key(data.uuid)
        return build_document(
            document_key=doc_key,
            document_type="ENDPOINT_STUDY_RECORD",
            document_sub_type=DOC_SUBTYPE,
            name=data.name,
            content_element=esr,
            parent_document_key=document_key(data.substance_uuid),
            order_in_section_no=1,
        )

    @staticmethod
    def _build_soil_structured_full(
        materials: ET.Element,
        props_entries: list[SoilPropertiesData],
    ) -> None:
        study_design = _sub(materials, NS_ESR_BIODEG, "StudyDesign")

        soil_classification = None
        for props in props_entries:
            soil_classification = EndpointStudyRecordBuilder._soil_classification(props)
            if soil_classification:
                break

        if soil_classification:
            soil_classification_el = _sub(study_design, NS_ESR_BIODEG, "SoilClassification")
            value, other = EndpointStudyRecordBuilder._picklist_value_and_other(
                soil_classification,
                SOIL_CLASSIFICATION_CODE_BY_KEY,
            )
            if value:
                _sub(soil_classification_el, NS_ESR_BIODEG, "value", value)
            if other:
                _sub(soil_classification_el, NS_ESR_BIODEG, "other", other)

        soil_props = _sub(study_design, NS_ESR_BIODEG, "SoilProperties")

        for props in props_entries:
            entry = ET.SubElement(soil_props, _tag(NS_ESR_BIODEG, "entry"))
            entry.set(_tag(NS_PLATFORM_FIELDS, "uuid"), str(uuid4()))

            if props.soil_no_code:
                soil_no = _sub(entry, NS_ESR_BIODEG, "SoilNo")
                _sub(soil_no, NS_ESR_BIODEG, "value", props.soil_no_code)

            soil_type = props.soil_type.strip() if props.soil_type else None
            if soil_type:
                soil_type_el = _sub(entry, NS_ESR_BIODEG, "SoilType")
                value, other = EndpointStudyRecordBuilder._picklist_value_and_other(
                    soil_type,
                    SOIL_TYPE_CODE_BY_KEY,
                )
                if value:
                    _sub(soil_type_el, NS_ESR_BIODEG, "value", value)
                if other:
                    _sub(soil_type_el, NS_ESR_BIODEG, "other", other)

            if props.clay is not None:
                clay_el = _sub(entry, NS_ESR_BIODEG, "Clay")
                _sub(clay_el, NS_ESR_BIODEG, "lowerValue", str(props.clay))

            if props.silt is not None:
                silt_el = _sub(entry, NS_ESR_BIODEG, "Silt")
                _sub(silt_el, NS_ESR_BIODEG, "lowerValue", str(props.silt))

            if props.sand is not None:
                sand_el = _sub(entry, NS_ESR_BIODEG, "Sand")
                _sub(sand_el, NS_ESR_BIODEG, "lowerValue", str(props.sand))

            if props.org_carbon is not None:
                orgc_el = _sub(entry, NS_ESR_BIODEG, "OrgC")
                _sub(orgc_el, NS_ESR_BIODEG, "lowerValue", str(props.org_carbon))

            if props.ph_lower is not None or props.ph_upper is not None:
                ph_el = _sub(entry, NS_ESR_BIODEG, "Ph")
                if props.ph_lower is not None:
                    _sub(ph_el, NS_ESR_BIODEG, "lowerValue", str(props.ph_lower))
                if props.ph_upper is not None:
                    _sub(ph_el, NS_ESR_BIODEG, "upperValue", str(props.ph_upper))

            ph_method = props.ph_method.strip() if props.ph_method else None
            if ph_method:
                _sub(entry, NS_ESR_BIODEG, "PHMeasuredIn", ph_method)

            if props.cec is not None:
                cec_el = _sub(entry, NS_ESR_BIODEG, "CEC")
                _sub(cec_el, NS_ESR_BIODEG, "lowerValue", str(props.cec))

            if props.moisture_content is not None:
                moisture_el = _sub(entry, NS_ESR_BIODEG, "MoistureContent")
                _sub(moisture_el, NS_ESR_BIODEG, "lowerValue", str(props.moisture_content))

    @staticmethod
    def _soil_classification(props: SoilPropertiesData) -> str | None:
        if props.soil_classification:
            value = props.soil_classification.strip()
            if value:
                return value
        if props.soil_type:
            return "USDA"
        return None

    @staticmethod
    def _picklist_value_and_other(
        raw_value: str,
        code_map: dict[str, str],
    ) -> tuple[str | None, str | None]:
        value = raw_value.strip()
        if not value:
            return None, None

        key = value.upper().replace("-", "_").replace(" ", "_")
        code = code_map.get(key)
        if code is not None:
            return code, None

        return PICKLIST_OTHER_CODE, value.replace("_", " ")
