"""Protected module containing the ECMind Blue object definition parser."""

from __future__ import annotations

import xml.etree.ElementTree as ET
from dataclasses import dataclass
from datetime import datetime
from typing import overload

from ecmind_blue_client.ecm.definition._ecm_cabinet_definition import (
    ECMCabinetDefinition,
)
from ecmind_blue_client.ecm.definition._ecm_object_definition import ECMObjectDefinition


@dataclass(frozen=True)
class ECMLanguageDefinition:
    """Represents a single language entry from the asobjdef language list.

    Attributes:
        lang_id: Numeric language identifier used throughout the definition XML.
        name: Human-readable language name (e.g. ``"German"``).
        iso_code: ISO locale name (e.g. ``"de_DE"``).
        is_active: ``True`` if this language is active (``active != "0"``).
        is_primary: ``True`` if this is the system primary language (``active="2"``).
    """

    lang_id: int
    name: str
    iso_code: str
    is_active: bool
    is_primary: bool


class ECMDefinition:
    """Parses and provides access to an ECMind Blue object definition (asobjdef XML).

    The definition XML is retrieved from the server once and then queried via helper
    properties and methods. All parsing happens eagerly in ``__init__``.

    Args:
        source: The asobjdef XML either as a plain Python string or as a
            ``JobResponseFile`` returned by the server. UTF-16 encoded byte
            payloads (from ``JobResponseFile``) are handled automatically.

    Raises:
        ValueError: If the XML does not contain a ``<languages>`` element or if no
            primary language (``active="2"``) is present.

    Example::

        # From an XML string
        with open("asobjdef.xml", encoding="utf-16") as f:
            definition = ECMDefinition(f.read())

        # From a JobResponseFile
        definition = ECMDefinition(job_response_file)

        print(definition.primary_language.iso_code)   # e.g. "de_DE"
        print(definition.available_languages)          # all active languages
    """

    def __init__(self, source: bytes | str) -> None:
        if isinstance(source, bytes):
            root = ET.fromstring(source)
        else:
            # JobResponseFile.bytes() returns bytes; expat handles UTF-16 BOM automatically.
            root = ET.fromstring(source)

        self._created: datetime = datetime.fromisoformat(root.get("created", ""))
        self._version: str = root.get("version", "")
        self._limited_objects: bool = bool(int(root.get("limited_objects", "0")))

        languages_elem = root.find("languages")
        if languages_elem is None:
            raise ValueError("asobjdef XML does not contain a <languages> element")

        self._primary_language: ECMLanguageDefinition | None = None
        self._available_languages: dict[str, ECMLanguageDefinition] = {}
        self._all_languages: dict[str, ECMLanguageDefinition] = {}

        for lang_elem in languages_elem.findall("language"):
            active = int(lang_elem.get("active", "0"))
            lang = ECMLanguageDefinition(
                lang_id=int(lang_elem.get("lang_id", "0")),
                name=lang_elem.get("name", ""),
                iso_code=lang_elem.get("localename", ""),
                is_active=(active != 0),
                is_primary=(active == 2),
            )
            self._all_languages[lang.iso_code] = lang
            if active == 0:
                continue
            self._available_languages[lang.iso_code] = lang
            if active == 2:
                self._primary_language = lang

        if self._primary_language is None:
            raise ValueError('asobjdef XML does not define a primary language (active="2")')

        all_languages = list(self._available_languages.values())
        self._cabinets: dict[str, ECMCabinetDefinition] = {
            cab.internal_name: cab
            for cab_elem in root.findall("cabinet")
            for cab in (ECMCabinetDefinition._from_xml(cab_elem, all_languages),)  # pylint: disable=protected-access
        }

    @property
    def created(self) -> datetime:
        """Timestamp when the definition was generated on the server.

        Returns:
            A ``datetime`` parsed from the ``created`` attribute of the root element.
        """
        return self._created

    @property
    def version(self) -> str:
        """Server version string from the definition XML.

        Returns:
            The ``version`` attribute of the root element (e.g. ``"5.20"``).
        """
        return self._version

    @property
    def limited_objects(self) -> bool:
        """Whether the definition only contains a limited set of object types.

        Returns:
            ``True`` if ``limited_objects="1"`` is set on the root element.
        """
        return self._limited_objects

    @property
    def primary_language(self) -> ECMLanguageDefinition:
        """The system primary language (``active="2"`` in the XML).

        Returns:
            The ``ECMLanguageDefinition`` for the primary language.
        """
        return self._primary_language  # type: ignore[return-value]  # guaranteed by __init__

    @property
    def available_languages(self) -> dict[str, ECMLanguageDefinition]:
        """All active languages keyed by ISO locale code, including the primary language.

        Returns:
            A dict mapping ISO locale code to ``ECMLanguageDefinition`` for all active languages.
        """
        return dict(self._available_languages)

    @property
    def all_languages(self) -> dict[str, ECMLanguageDefinition]:
        """All languages keyed by ISO locale code, including inactive ones.

        Returns:
            A dict mapping ISO locale code to ``ECMLanguageDefinition`` regardless of active state.
        """
        return dict(self._all_languages)

    @property
    def cabinets(self) -> dict[str, ECMCabinetDefinition]:
        """All cabinet definitions keyed by internal name.

        Returns:
            A dict mapping ``internal_name`` to ``ECMCabinetDefinition``.
        """
        return dict(self._cabinets)

    @overload
    def __getitem__(self, identifier: str) -> ECMObjectDefinition: ...

    @overload
    def __getitem__(self, identifier: int) -> ECMObjectDefinition: ...

    def __getitem__(self, identifier: str | int) -> ECMObjectDefinition:
        """Return the object type definition for the given identifier.

        Accepts either the internal name (``str``) or the composite type ID (``int``).
        The composite type ID is ``(maintype.value << 16) | cotype`` and matches the
        value returned by ``dms.get_object_type_by_id()`` and stored in
        ``ECMObjectDefinition.type_id``.

        Args:
            identifier: Internal name of the object type, or its composite type ID.

        Returns:
            The matching ``ECMObjectDefinition``.

        Raises:
            KeyError: If no object type with the given identifier exists.
        """
        if isinstance(identifier, int):
            for cabinet in self._cabinets.values():
                for obj in cabinet.objects.values():
                    if obj.type_id == identifier:
                        return obj
            raise KeyError(identifier)
        for cabinet in self._cabinets.values():
            obj = cabinet.objects.get(identifier)
            if obj is not None:
                return obj
        raise KeyError(identifier)
