"""Abstract base class and shared XML builder helpers for the high-level ECM API."""

from abc import ABC
from datetime import datetime
from typing import TYPE_CHECKING, Any, TypeVar

from ecmind_blue_client import SystemFields
from ecmind_blue_client.pool import AsyncPoolClient, SyncPoolClient

if TYPE_CHECKING:
    from ecmind_blue_client.ecm.model._ecm_model import _ECMModelBase
    from ecmind_blue_client.ecm.model._ecm_model_field import ECMCondition

ClientT = TypeVar("ClientT", bound=SyncPoolClient | AsyncPoolClient)


class ECMAbstract[ClientT](ABC):  # pylint: disable=too-few-public-methods
    """Abstract base class for all high-level ECM API classes.

    Wraps a pool client and exposes it to subclasses via the ``client`` property.
    Concrete implementations (e.g. ``ECMSync``, ``ECMAsync``) inherit from this class
    and specialise it with the appropriate ``ClientT`` type argument
    (``SyncPoolClient`` or ``AsyncPoolClient``).

    Type Args:
        ClientT: Either ``SyncPoolClient`` or ``AsyncPoolClient``.
    """

    def __init__(self, client: ClientT) -> None:
        """Initialise the ECM instance with a pool client.

        Args:
            client: The pool client used to communicate with the ECMind Blue server.
        """
        self._client = client
        self._impersonate_username: str | None = None

    @property
    def client(self) -> ClientT:
        """The underlying pool client.

        Returns:
            The pool client instance passed to the constructor.
        """
        return self._client

    def _inject_params(self, kwargs: dict[str, Any]) -> dict[str, Any]:
        """Hook to inject additional parameters into every ``client.execute`` call.

        Override this method in a subclass to add default parameters (e.g. tenant IDs,
        custom flags, tracing headers) to every server request without modifying each
        call site individually.

        The default implementation injects the ``$$$SwitchContextUserName$$$`` parameter
        when ``as_user()`` was called on this instance. Subclasses that override this method
        should call ``super()._inject_params(kwargs)`` to preserve that behaviour.

        Args:
            kwargs: The keyword arguments about to be passed to ``client.execute``.

        Returns:
            The (possibly modified) kwargs dict forwarded to ``client.execute``.

        Example:
            ::

                class MyECM(ECMSync):
                    def _inject_params(self, kwargs):
                        kwargs = super()._inject_params(kwargs)
                        kwargs.setdefault("Language", "DE")
                        return kwargs
        """
        if self._impersonate_username is not None:
            kwargs["$$$SwitchContextUserName$$$"] = self._impersonate_username
        return kwargs

    @staticmethod
    def _check_required_fields(model: "_ECMModelBase") -> None:
        """Raise ``ValueError`` if any ``mandatory=True`` ECMField has no value set.

        Iterates over all ``ECMField`` descriptors in the model's MRO and collects
        those marked ``mandatory=True`` whose value in ``_raw_fields_`` is ``None``.

        Args:
            model: The model instance to validate.

        Raises:
            ValueError: Listing all missing mandatory field names.
        """
        from ecmind_blue_client.ecm.model._ecm_model_field import (  # pylint: disable=import-outside-toplevel
            ECMField,
        )

        missing: list[str] = []
        seen: set[str] = set()
        for klass in type(model).__mro__:
            for attr_val in klass.__dict__.values():
                if isinstance(attr_val, ECMField) and attr_val.mandatory:
                    name = attr_val._internal_name  # pylint: disable=protected-access
                    if name is not None and name not in seen:
                        seen.add(name)
                        if model._raw_fields_.get(name) is None:  # pylint: disable=protected-access
                            missing.append(name)
        if missing:
            raise ValueError(f"Required fields not set on {type(model).__name__}: {', '.join(sorted(missing))}")

    @staticmethod
    def _build_insert_xml(  # pylint: disable=too-many-locals
        model: "_ECMModelBase",
        folder_id: int | None = None,
        register_id: int | None = None,
    ) -> bytes:
        """Build the DMSData XML for a ``dms.XMLInsert`` request from a model instance.

        Serialises all non-``None`` scalar fields and all non-empty table fields from the model
        into a ``DmsDataBuilder`` XML structure.

        Args:
            model: The model instance whose field values are serialised.
            folder_id: Optional ID of the folder to place the new object in.
            register_id: Optional ID of the register (or parent register for sub-registers)
                to place the new object in.

        Returns:
            UTF-8 encoded DMSData XML bytes ready to pass as the ``XML`` job parameter.
        """
        from ecmind_blue_client.ecm.builder._dms_data_builder import (  # pylint: disable=import-outside-toplevel
            DmsDataBuilder,
        )

        identifier = model._ecm_identifier()  # pylint: disable=protected-access
        raw_fields = model._raw_fields_  # pylint: disable=protected-access
        raw_table_fields = model._raw_table_fields_  # pylint: disable=protected-access

        builder = DmsDataBuilder()
        obj = builder.object(identifier)

        if folder_id is not None:
            obj.folder_id = folder_id
        if register_id is not None:
            obj.register_id = register_id

        # Add concurrency_timestamp if the object has a lastmodified property
        last_modified = raw_fields.get("OBJECT_MODIFYTIME")
        if last_modified is not None:

            if isinstance(last_modified, datetime):
                obj.concurrency_timestamp = int(last_modified.timestamp())

        for field_name, value in raw_fields.items():
            if value is not None and field_name not in SystemFields.__members__:
                obj.field(field_name, value)

        for tf_name, rows in raw_table_fields.items():
            if rows:
                tf = obj.table_field(tf_name)
                for row in rows:
                    row_fields = {k: v for k, v in row._raw_fields_.items() if v is not None}  # pylint: disable=protected-access
                    if row_fields:
                        tf.row(row_fields)

        return builder.to_xml()

    @staticmethod
    def _build_update_xml(model: "_ECMModelBase") -> bytes:
        """Build the DMSData XML for a ``dms.XMLUpdate`` request from a model instance.

        Serialises all non-``None`` scalar fields and all non-empty table fields from the model
        into a ``DmsDataBuilder`` XML structure, with the model's object ID set for identification.

        Args:
            model: The model instance whose field values are serialised. Must have ``id`` set.

        Returns:
            UTF-8 encoded DMSData XML bytes ready to pass as the ``XML`` job parameter.

        Raises:
            ValueError: If ``model.id`` is ``None``.
        """
        from ecmind_blue_client.ecm.builder._dms_data_builder import (  # pylint: disable=import-outside-toplevel
            DmsDataBuilder,
        )

        if model._id_ is None:  # pylint: disable=protected-access
            raise ValueError(
                f"Cannot update {type(model).__name__}: model has no id. " "Load the object from the server first or set the id manually."
            )

        identifier = model._ecm_identifier()  # pylint: disable=protected-access
        raw_fields = model._raw_fields_  # pylint: disable=protected-access
        raw_table_fields = model._raw_table_fields_  # pylint: disable=protected-access

        builder = DmsDataBuilder()
        obj = builder.object(identifier, object_id=model._id_)  # pylint: disable=protected-access

        # Add concurrency_timestamp if the object has a lastmodified property
        last_modified = raw_fields.get("OBJECT_MODIFYTIME")
        if last_modified is not None:
            if isinstance(last_modified, datetime):
                obj.concurrency_timestamp = int(last_modified.timestamp())

        for field_name, value in raw_fields.items():
            if value is not None and field_name not in SystemFields.__members__:
                obj.field(field_name, value)

        for tf_name, rows in raw_table_fields.items():
            if rows:
                tf = obj.table_field(tf_name)
                for row in rows:
                    row_fields = {k: v for k, v in row._raw_fields_.items() if v is not None}  # pylint: disable=protected-access
                    if row_fields:
                        tf.row(row_fields)

        return builder.to_xml()

    @staticmethod
    def _build_upsert_xml(  # pylint: disable=too-many-locals
        model: "_ECMModelBase",
        search_conditions: "list[ECMCondition]",
        folder_id: int | None = None,
        register_id: int | None = None,
    ) -> bytes:
        """Build the DMSData XML for a ``dms.XMLImport`` (upsert) request from a model instance.

        Serialises all non-``None`` scalar fields and all non-empty table fields from the model
        into a ``DmsDataBuilder`` XML structure, and adds a ``<Search>`` section populated from
        ``search_conditions`` for server-side deduplication.

        Args:
            model: The model instance whose field values are serialised.
            search_conditions: ``ECMCondition`` instances used as equality search criteria.
                Only ``field_name`` and ``value`` are used; operators are ignored.
            folder_id: Optional ID of the folder to place the new object in.
            register_id: Optional ID of the register (or parent register for sub-registers)
                to place the new object in.

        Returns:
            UTF-8 encoded DMSData XML bytes ready to pass as the ``XML`` job parameter.
        """
        from ecmind_blue_client.ecm.builder._dms_data_builder import (  # pylint: disable=import-outside-toplevel
            DmsDataBuilder,
        )

        identifier = model._ecm_identifier()  # pylint: disable=protected-access
        raw_fields = model._raw_fields_  # pylint: disable=protected-access
        raw_table_fields = model._raw_table_fields_  # pylint: disable=protected-access

        builder = DmsDataBuilder()
        obj = builder.object(identifier)

        if folder_id is not None:
            obj.folder_id = folder_id
        if register_id is not None:
            obj.register_id = register_id

        for field_name, value in raw_fields.items():
            if value is not None:
                obj.field(field_name, value)

        for tf_name, rows in raw_table_fields.items():
            if rows:
                tf = obj.table_field(tf_name)
                for row in rows:
                    row_fields = {k: v for k, v in row._raw_fields_.items() if v is not None}  # pylint: disable=protected-access
                    if row_fields:
                        tf.row(row_fields)

        for condition in search_conditions:
            obj.search_field(condition.field_name, condition.value)

        return builder.to_xml()

    @staticmethod
    def _build_move_xml(
        object_id: int,
        object_identifier: int | str,
        folder_id: int | None = None,
        register_id: int | None = None,
        register_type: int | None = None,
    ) -> bytes:
        """Build the DMSData XML for a ``dms.XMLMove`` request.

        Creates a minimal ``DmsDataBuilder`` XML structure containing only the object type
        identifier, the object ID, and the new location attributes.

        Args:
            object_id: Numeric ID of the object to move.
            object_identifier: Internal name (``str``) or type ID (``int``) that identifies
                the ``<ObjectType>`` element in the DMSData XML.
            folder_id: ID of the target folder.
            register_id: ID of the target register. Pass ``-1`` to move directly into a
                folder without a register (matches the documented XML convention).
            register_type: Type ID of the target register. Pass ``-1`` together with
                ``register_id=-1`` when targeting a folder directly.

        Returns:
            UTF-8 encoded DMSData XML bytes ready to pass as the ``XML`` job parameter.
        """
        from ecmind_blue_client.ecm.builder._dms_data_builder import (  # pylint: disable=import-outside-toplevel
            DmsDataBuilder,
        )

        builder = DmsDataBuilder()
        obj = builder.object(object_identifier, object_id=object_id)

        if folder_id is not None:
            obj.folder_id = folder_id
        if register_id is not None:
            obj.register_id = register_id
        if register_type is not None:
            obj.register_type = register_type

        return builder.to_xml()

    @staticmethod
    def _build_delete_xml(object_id: int, object_type: "int | str") -> bytes:
        """Build the DMSData XML for a ``dms.XMLDelete`` request.

        Creates a minimal ``DmsDataBuilder`` XML structure containing only the object type
        identifier and the object ID required to identify the object to delete.

        Args:
            object_id: Numeric ID of the object to delete.
            object_type: Internal name (``str``) or numeric type ID (``int``) of the object.

        Returns:
            UTF-8 encoded DMSData XML bytes ready to pass as the ``XML`` job parameter.
        """
        from ecmind_blue_client.ecm.builder._dms_data_builder import (  # pylint: disable=import-outside-toplevel
            DmsDataBuilder,
        )

        builder = DmsDataBuilder()
        builder.object(object_type, object_id=object_id)
        return builder.to_xml()
