"""Protected module containing the ECMCabinetDefinition dataclass and its XML factory."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Iterator
from xml.etree.ElementTree import Element

from ecmind_blue_client.ecm.definition._ecm_description_entry import ECMDescriptionEntry
from ecmind_blue_client.ecm.definition._ecm_field_definition import (
    ECMFieldDefinition,
    ECMFieldList,
    ECMFieldPosition,
    ECMListNodeEntry,
    ECMListRowEntry,
    ECMPageDefinition,
    ECMTableColumnDefinition,
)
from ecmind_blue_client.ecm.definition._ecm_object_definition import (
    ECMCompressionMode,
    ECMDocumentDefinition,
    ECMFolderDefinition,
    ECMFulltextMode,
    ECMHistoryFlags,
    ECMLimitedObjectEntry,
    ECMMainType,
    ECMObjectDefinition,
    ECMRegisterDefinition,
)

if TYPE_CHECKING:
    from collections.abc import Sequence

    from ecmind_blue_client.ecm.definition._ecm_definition import ECMLanguageDefinition


def _parse_descriptions(
    elem: Element, lang_map: "dict[int, ECMLanguageDefinition]"
) -> "tuple[ECMDescriptionEntry | None, dict[str, ECMDescriptionEntry]]":
    """Parse a ``<names>/<name>`` subtree into a primary entry and an ISO-keyed mapping.

    Args:
        elem: The parent XML element that may contain a ``<names>`` child.
        lang_map: Mapping of ``lang_id`` to ``ECMLanguageDefinition`` for lookup.

    Returns:
        A tuple of ``(primary_entry, all_entries)`` where ``primary_entry`` is the entry
        for the primary language (or ``None``) and ``all_entries`` maps ISO locale codes to entries.
    """
    names_elem = elem.find("names")
    descriptions: dict[str, ECMDescriptionEntry] = {}
    description: ECMDescriptionEntry | None = None
    if names_elem is not None:
        for name_elem in names_elem.findall("name"):
            lang_id = int(name_elem.get("lang_id", "0"))
            lang = lang_map.get(lang_id)
            if lang is None:
                continue
            entry = ECMDescriptionEntry(
                name=name_elem.text or "",
                tooltip=name_elem.get("tooltip", ""),
                image_id=name_elem.get("imageid", ""),
                default_url=name_elem.get("defaulturl", ""),
                new_url=name_elem.get("newurl", ""),
                update_url=name_elem.get("updateurl", ""),
                request_url=name_elem.get("requesturl", ""),
                readonly_url=name_elem.get("readonlyurl", ""),
            )
            descriptions[lang.iso_code] = entry
            if lang.is_primary:
                description = entry
    return description, descriptions


def _parse_page(elem: Element, lang_map: "dict[int, ECMLanguageDefinition]") -> ECMPageDefinition:
    """Parse a single ``<page>`` XML element into an ``ECMPageDefinition``.

    Args:
        elem: The ``<page>`` XML element.
        lang_map: Mapping of ``lang_id`` to ``ECMLanguageDefinition`` for lookup.

    Returns:
        A populated ``ECMPageDefinition`` instance.
    """
    description, descriptions = _parse_descriptions(elem, lang_map)
    fields = {
        f.get("internal", ""): _parse_field(f, lang_map) for fields_elem in elem.findall("fields") for f in fields_elem.findall("field")
    }
    return ECMPageDefinition(
        page_id=elem.get("page_id", ""),
        internal_name=elem.get("internal", ""),
        position=int(elem.get("pos", "0")),
        description=description,
        descriptions=descriptions,
        fields=fields,
    )


def _parse_listctrl(elem: Element, lang_map: "dict[int, ECMLanguageDefinition]") -> ECMTableColumnDefinition:
    """Parse a single ``<listctrl>`` XML element into an ``ECMTableColumnDefinition``.

    Args:
        elem: The ``<listctrl>`` XML element.
        lang_map: Mapping of ``lang_id`` to ``ECMLanguageDefinition`` for lookup.

    Returns:
        A populated ``ECMTableColumnDefinition`` instance.
    """
    description, descriptions = _parse_descriptions(elem, lang_map)
    return ECMTableColumnDefinition(
        internal_name=elem.get("internal", ""),
        os_guid=elem.get("os_guid", ""),
        field_name=elem.get("fieldname", ""),
        _type=elem.get("type", ""),
        id=int(elem.get("id", "0")),
        length=int(elem.get("length", "0")),
        col_width=int(elem.get("colwidth", "0")),
        col_pos=int(elem.get("colpos", "0")),
        color=int(elem.get("color", "0")),
        text_align=int(elem.get("textalign", "0")),
        catalog_type=int(elem.get("catalogtype", "0")),
        link_id=int(elem.get("link_id", "0")),
        readonly=elem.get("readonly", "0") != "0",
        values_id=elem.get("values_id", ""),
        description=description,
        descriptions=descriptions,
    )


def _parse_list_nodes(nodes_elem: Element) -> list[ECMListNodeEntry]:
    """Recursively parse a ``<nodes>`` XML element into a list of ``ECMListNodeEntry`` instances.

    Args:
        nodes_elem: A ``<nodes>`` XML element whose ``<node>`` children are to be parsed.

    Returns:
        Ordered list of ``ECMListNodeEntry`` instances, each potentially containing children.
    """
    result = []
    for node_elem in nodes_elem.findall("node"):
        child_nodes_elem = node_elem.find("nodes")
        children = _parse_list_nodes(child_nodes_elem) if child_nodes_elem is not None else []
        result.append(
            ECMListNodeEntry(
                index=int(node_elem.get("index", "0")),
                name=node_elem.get("name", ""),
                value=node_elem.get("value", ""),
                children=children,
            )
        )
    return result


def _parse_list(elem: Element) -> ECMFieldList:
    """Parse a ``<list>`` XML element into an ``ECMFieldList``.

    Args:
        elem: The ``<list>`` XML element.

    Returns:
        A populated ``ECMFieldList`` instance.
    """
    rows = [ECMListRowEntry(index=int(r.get("index", "0")), value=r.text or "") for r in elem.findall("row")]
    nodes_elem = elem.find("nodes")
    nodes = _parse_list_nodes(nodes_elem) if nodes_elem is not None else []
    _uin = elem.find("USEINTERMEDIATENODES")
    _mm = elem.find("MATCHMODE")
    _w = elem.find("WIDTH")
    _h = elem.find("HEIGHT")
    _addon = elem.find("ADDON32")
    _raw = elem.find("rawdata")
    return ECMFieldList(
        multiselection=elem.get("multiselection", "0") != "0",
        order=int(elem.get("order", "0")),
        keyselcol=int(elem.get("keyselcol", "0")),
        sortcol=int(elem.get("sortcol", "0")),
        separator=elem.get("separator", ""),
        rows=rows,
        nodes=nodes,
        use_intermediate_nodes=(_uin.text or "").strip().lower() == "ja" if _uin is not None else False,
        match_mode=int(_mm.text or "0") if _mm is not None else 0,
        width=int(_w.text or "0") if _w is not None else 0,
        height=int(_h.text or "0") if _h is not None else 0,
        addon32=(_addon.text or "").strip() if _addon is not None else "",
        rawdata=(_raw.text or "") if _raw is not None else "",
    )


def _parse_field(elem: Element, lang_map: "dict[int, ECMLanguageDefinition]") -> ECMFieldDefinition:
    """Parse a single ``<field>`` XML element into an ``ECMFieldDefinition``.

    Args:
        elem: The ``<field>`` XML element.
        lang_map: Mapping of ``lang_id`` to ``ECMLanguageDefinition`` for lookup.

    Returns:
        A populated ``ECMFieldDefinition`` instance.

    Raises:
        ValueError: If no primary language description entry is found for the field.
    """
    description, descriptions = _parse_descriptions(elem, lang_map)
    if description is None:
        raise ValueError(f"Field '{elem.get('internal', '')}' has no primary language description entry.")

    def _parse_pos(tag: str) -> ECMFieldPosition:
        pos = elem.find(tag)
        if pos is None:
            return ECMFieldPosition(left=0, top=0, right=0, bottom=0)
        return ECMFieldPosition(
            left=int(pos.get("left", "0")),
            top=int(pos.get("top", "0")),
            right=int(pos.get("right", "0")),
            bottom=int(pos.get("bottom", "0")),
        )

    flags_elem = elem.find("flags")
    raw_input_length = flags_elem.get("input_length") if flags_elem is not None else None
    raw_case = flags_elem.get("case") if flags_elem is not None else None

    return ECMFieldDefinition(
        tab_order=int(elem.get("taborder", "0")),
        internal_name=elem.get("internal", ""),
        field_name=elem.get("fieldname", ""),
        prn_alias=elem.get("prnalias", ""),
        os_guid=elem.get("os_guid", ""),
        class_string=elem.get("classstring", ""),
        init=elem.get("init", ""),
        search_group_name=elem.get("searchgroupname", ""),
        regular_expression=elem.get("regularexpression", ""),
        label_position=_parse_pos("field_pos"),
        control_position=_parse_pos("input_pos"),
        input_length=int(raw_input_length) if raw_input_length is not None else None,
        flags=int(flags_elem.get("flags", "0")) if flags_elem is not None else 0,
        flags1=int(flags_elem.get("flags1", "0")) if flags_elem is not None else 0,
        flags2=int(flags_elem.get("flags2", "0")) if flags_elem is not None else 0,
        dt=flags_elem.get("dt", "") if flags_elem is not None else "",
        _datatype=flags_elem.get("datatype", "") if flags_elem is not None else "",
        _align=flags_elem.get("align", "") if flags_elem is not None else "",
        case=int(raw_case) if raw_case is not None else None,
        _control_type=flags_elem.get("control_type", "") if flags_elem is not None else "",
        description=description,
        descriptions=descriptions,
        pages={p.internal_name: p for p in (_parse_page(e, lang_map) for e in elem.findall("page"))},
        columns={c.internal_name: c for c in (_parse_listctrl(e, lang_map) for e in elem.findall("listctrl"))},
        list_values=_parse_list(list_elem) if (list_elem := elem.find("list")) is not None else None,
    )


def _parse_object(elem: Element, languages: "Sequence[ECMLanguageDefinition]") -> ECMObjectDefinition:
    """Parse a single ``<object>`` XML element into the matching ``ECMObjectDefinition`` subclass.

    Args:
        elem: The ``<object>`` XML element.
        languages: All active languages from the definition, used to build the name map.

    Returns:
        An ``ECMFolderDefinition``, ``ECMRegisterDefinition``, or ``ECMDocumentDefinition``
        depending on the ``maintype`` attribute.

    Raises:
        ValueError: If the ``maintype`` value is not a known ``ECMMainType`` member.
    """
    maintype = ECMMainType(int(elem.get("maintype", "0")))
    lang_map = {lang.lang_id: lang for lang in languages}
    description, descriptions = _parse_descriptions(elem, lang_map)
    _fr = elem.find("frame")
    _mf = elem.find("multiframe")
    kwargs = {
        "maintype": maintype,
        "cotype": int(elem.get("cotype", "0")),
        "internal_name": elem.get("internal", ""),
        "os_guid": elem.get("os_guid", ""),
        "tablename": elem.get("tablename", ""),
        "ext_tablename": elem.get("extablename", ""),
        "icon_id": int(elem.get("IconID", "0")),
        "history_flags": ECMHistoryFlags._from_int(int(elem.get("historyflags", "0"))),  # pylint: disable=protected-access
        "compression_flags": ECMCompressionMode(int(elem.get("compressionflags", "0"))),
        "reference_type": int(elem.get("referencetype", "0")),
        "ki_search_index": bool(int(elem.get("kisearchindex", "0"))),
        "no_dias": bool(int(elem.get("no_dias", "0"))),
        "fulltext": ECMFulltextMode(int(elem.get("fulltext", "0"))),
        "description": description,
        "descriptions": descriptions,
        "fields": {
            f.get("internal", ""): _parse_field(f, lang_map) for fields_elem in elem.findall("fields") for f in fields_elem.findall("field")
        },
        "limited_objects": [
            ECMLimitedObjectEntry(object_type_id=int(lo.get("type", "0")), count=int(lo.get("count", "0")))
            for lo_elem in elem.findall("limited_objects")
            for lo in lo_elem.findall("limited_object")
        ],
        "frame_left": int(_fr.get("left", "0")) if _fr is not None else 0,
        "frame_top": int(_fr.get("top", "0")) if _fr is not None else 0,
        "frame_right": int(_fr.get("right", "0")) if _fr is not None else 0,
        "frame_bottom": int(_fr.get("bottom", "0")) if _fr is not None else 0,
        "multiframe_width": int(_mf.get("width", "0")) if _mf is not None else 0,
        "multiframe_height": int(_mf.get("height", "0")) if _mf is not None else 0,
    }
    if maintype == ECMMainType.FOLDER:
        return ECMFolderDefinition(**kwargs)
    if maintype == ECMMainType.REGISTER:
        return ECMRegisterDefinition(**kwargs)
    return ECMDocumentDefinition(**kwargs, cross_module_document_type=bool(int(elem.get("multidoc", "0"))))


@dataclass(frozen=True)
class ECMCabinetDefinition:
    """Represents a single cabinet (``<cabinet>``) from the asobjdef XML.

    A cabinet is the top-level grouping of ECM object types. Each cabinet contains
    folder types, register types, and document types as ``<object>`` children.

    Attributes:
        cotype: Numeric cabinet type ID.
        name: Display name of the cabinet.
        internal_name: Internal (programmatic) name of the cabinet.
        objects: All object type definitions belonging to this cabinet, keyed by internal name.
    """

    cotype: int
    name: str
    internal_name: str
    objects: dict[str, ECMObjectDefinition]

    @staticmethod
    def _from_xml(elem: Element, languages: "Sequence[ECMLanguageDefinition]") -> ECMCabinetDefinition:
        """Parse a ``<cabinet>`` XML element into an ``ECMCabinetDefinition``.

        Args:
            elem: The ``<cabinet>`` XML element.
            languages: All active languages from the definition, forwarded to each object parser.

        Returns:
            A populated ``ECMCabinetDefinition`` instance.
        """
        objects = {obj.internal_name: obj for obj_elem in elem.findall("object") for obj in (_parse_object(obj_elem, languages),)}
        return ECMCabinetDefinition(
            cotype=int(elem.get("cotype", "0")),
            name=elem.get("name", ""),
            internal_name=elem.get("internal", ""),
            objects=objects,
        )

    def __getitem__(self, key: str) -> ECMObjectDefinition:
        return self.objects[key]

    def __contains__(self, key: object) -> bool:
        return key in self.objects

    def __iter__(self) -> Iterator[str]:
        return iter(self.objects)

    def __len__(self) -> int:
        return len(self.objects)

    def keys(self):
        """Return the internal names of all object type definitions."""
        return self.objects.keys()

    def values(self):
        """Return all object type definitions."""
        return self.objects.values()

    def items(self):
        """Return all ``(internal_name, definition)`` pairs."""
        return self.objects.items()

    @property
    def folders(self) -> dict[str, ECMFolderDefinition]:
        """All folder type definitions in this cabinet, keyed by internal name."""
        return {k: o for k, o in self.objects.items() if isinstance(o, ECMFolderDefinition)}

    @property
    def registers(self) -> dict[str, ECMRegisterDefinition]:
        """All register type definitions in this cabinet, keyed by internal name."""
        return {k: o for k, o in self.objects.items() if isinstance(o, ECMRegisterDefinition)}

    @property
    def documents(self) -> dict[str, ECMDocumentDefinition]:
        """All document type definitions in this cabinet, keyed by internal name."""
        return {k: o for k, o in self.objects.items() if isinstance(o, ECMDocumentDefinition)}
