"""Abstract base class for DMSContent XML parsers."""
import xml.etree.ElementTree as ET
from abc import ABC, abstractmethod
from datetime import datetime
from typing import TypeVar

from ecmind_blue_client.rpc import JobResponseFile
from ecmind_blue_client.ecm.parser._dms_content_message import DmsContentMessage
from ecmind_blue_client.ecm.parser._dms_content_typeless_object import DmsContentTypelessObject

T = TypeVar("T")


class DmsContentParser[T](ABC):
    """Abstract base parser for the DMSContent XML returned by ``dms.GetResultList``.

    Accepts either raw UTF-16 bytes or a ``JobResponseFile`` object. Subclasses
    specialise for HOL (``DmsContentParserHol``) or LOL (``DmsContentParserLol``) format.

    Use as a context manager or call ``object_types()`` / ``object_type()`` directly.
    """

    def __init__(self, xml: bytes | JobResponseFile):
        """Args:
            xml: Raw UTF-16 XML bytes or a ``JobResponseFile`` from the RPC layer.

        Raises:
            TypeError: If *xml* is neither ``bytes`` nor ``JobResponseFile``.
        """
        if isinstance(xml, bytes):
            xml_string = xml.decode("utf-16")
        elif isinstance(xml, JobResponseFile):
            xml_string = xml.bytes().decode("utf-16")
        else:
            raise TypeError("Expected bytes or JobResponseFile")

        self._node = ET.fromstring(xml_string)
        self._pc_list = {c: p for p in self._node.iter() for c in p}

    @property
    def format(self) -> str:
        """Response format string (e.g. ``HOL``, ``LOL``)."""
        return self._node.get("format", "")

    @property
    def version(self) -> str:
        """DMSContent schema version string."""
        return self._node.get("version", "")

    @property
    def timestamp(self) -> datetime:
        """Server-side ISO 8601 timestamp of when the response was generated."""
        return datetime.fromisoformat(self._node.get("timestamp", ""))

    @property
    def user(self) -> str:
        """Login name of the user whose session produced this response."""
        return self._node.get("user", "")

    @property
    def station(self) -> str:
        """Station (client host) identifier from the server session."""
        return self._node.get("station", "")

    @property
    def instance(self) -> str:
        """Server instance name that generated this response."""
        return self._node.get("instance", "")

    @property
    def output_language(self) -> int:
        """Numeric output language ID used for display value translations."""
        return int(self._node.get("output_language", "0"))

    @property
    def system_id(self) -> int:
        """Numeric ID of the ECM system that generated this response."""
        return int(self._node.get("system_id", "-1"))

    @property
    def lang_id(self) -> int:
        """Language ID used for the server response (default: 7)."""
        return int(self._node.get("lang_id", "7"))

    @property
    def typeless_objects(self) -> list[DmsContentTypelessObject]:
        """Returns all ``<TypelessObject>`` elements from the ``<TypelessObjects>`` section.

        Typeless objects are not assigned to an archive object type and typically
        appear in personal inboxes or workflow trays.

        Returns:
            list[DmsContentTypelessObject]: Parsed typeless objects, may be empty.
        """
        container = self._node.find("TypelessObjects")
        if container is None:
            return []
        return [
            DmsContentTypelessObject(
                id=int(obj.get("id", "-1")),
                maintype=int(obj.get("maintype", "-1")),
                module=obj.get("module", ""),
                user=obj.get("user", ""),
                pagecount=int(obj.get("pagecount", "0")),
                filesize=int(obj.get("filesize", "0")),
                mimetypeid=int(obj.get("mimetypeid", "-1")),
                timestamp=obj.get("timestamp", ""),
            )
            for obj in container.findall("TypelessObject")
        ]

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    @property
    def messages(self) -> list[DmsContentMessage]:
        """Returns all ``<Message>`` elements from the ``<Messages>`` section.

        The ``<Messages>`` element is always present in a DMSContent response.
        An empty list means the server reported no warnings or errors.
        A non-zero ``faultcode`` on a message indicates a server-side problem.

        Returns:
            list[DmsContentMessage]: Parsed server messages, may be empty.
        """
        messages_node = self._node.find("Messages")
        if messages_node is None:
            return []
        return [
            DmsContentMessage(
                faultcode=int(msg.get("faultcode", "0")),
                sourcecode=int(msg.get("sourcecode", "0")),
                text=msg.text or "",
            )
            for msg in messages_node.findall("Message")
        ]

    def object_types(self):
        """Yields all ``<ObjectType>`` elements from all ``<Archive>`` sections.

        Yields:
            T: Parsed object types (HOL or LOL, depending on the subclass).
        """
        nodes = self._node.findall("Archive/ObjectType")
        for node in nodes:
            archive = self._pc_list.get(node)
            if archive is not None:
                yield self._init_object_type(node, archive)

    def object_type(self, internal_name_or_id: str | int) -> T:
        """Returns a single ``<ObjectType>`` element by internal name or numeric ID.

        Args:
            internal_name_or_id: String internal name or integer ID to look up.

        Returns:
            T: The matching parsed object type.

        Raises:
            ValueError: If no ``<ObjectType>`` matching the given identifier is found.
        """
        if isinstance(internal_name_or_id, int):
            node = self._node.find(f"Archive/ObjectType[@id='{internal_name_or_id}']", None)
        else:
            node = self._node.find(f"Archive/ObjectType[@internal_name='{internal_name_or_id}']", None)

        if node is not None:
            archive = self._pc_list.get(node)
            if archive is not None:
                return self._init_object_type(node, archive)

        raise ValueError(f"No object type found with the given internal name or ID: {internal_name_or_id!r}")

    @abstractmethod
    def _init_object_type(self, node: ET.Element, archive: ET.Element) -> T:
        """Creates a concrete object type instance from the given XML elements.

        Args:
            node: The ``<ObjectType>`` XML element.
            archive: The parent ``<Archive>`` XML element.

        Returns:
            T: The concrete object type (HOL or LOL variant).
        """
