"""Asynchronous typed model upsert builder for the ECM API."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Generic, TypeVar

from ecmind_blue_client import Jobs
from ecmind_blue_client.ecm.model._ecm_model_field import ECMCondition

if TYPE_CHECKING:
    from ecmind_blue_client.ecm.model._ecm_model import ECMDocumentModel, _ECMModelBase
    from ecmind_blue_client.rpc import JobRequestFile

_AnyModelT = TypeVar("_AnyModelT", bound="_ECMModelBase")

_ACTION0_DEFAULT = "INSERT"
_ACTION1_DEFAULT = "UPDATE"
_ACTIONM_DEFAULT = "ERROR"


class ECMModelUpsertAsync(Generic[_AnyModelT]):  # pylint: disable=too-many-instance-attributes
    """Asynchronous upsert builder for declarative ECM model classes (``dms.XMLImport``).

    Executes a server-side search based on the conditions given via ``search()``, then
    inserts or updates the object depending on the number of results and the configured
    action parameters.

    Do not instantiate directly — use ``ECMAsync.upsert()``.

    Args:
        ecm: ``ECMAsync`` instance used to execute the upsert.
        model: Populated model instance whose field values are written to the server.
    """

    def __init__(
        self,
        ecm: Any,
        model: _AnyModelT,
        folder_id: int | None = None,
        register_id: int | None = None,
        check_mandatory: bool = True,
    ) -> None:
        self._ecm = ecm
        self._model = model
        self._folder_id = folder_id
        self._register_id = register_id
        self._check_mandatory = check_mandatory
        self._search_conditions: list[ECMCondition] = []
        self._action0: str = _ACTION0_DEFAULT
        self._action1: str = _ACTION1_DEFAULT
        self._action_multiple: str = _ACTIONM_DEFAULT
        self._files: list[JobRequestFile] | None = None
        self._replace_files: bool = True

    def search(self, *conditions: ECMCondition) -> ECMModelUpsertAsync[_AnyModelT]:
        """Add field conditions used to search for an existing object on the server.

        Only the ``field_name`` and ``value`` of each condition are used; operators are
        ignored because the server performs equality matching in the ``<Search>`` section.

        Args:
            *conditions: ``ECMCondition`` instances (created via ``Model.Field == value``).
        """
        self._search_conditions.extend(conditions)
        return self

    def action0(self, value: str = _ACTION0_DEFAULT) -> ECMModelUpsertAsync[_AnyModelT]:
        """Set the action performed when the search returns zero results.

        Args:
            value: One of ``"INSERT"`` (default), ``"NONE"``, ``"ERROR"``.
        """
        self._action0 = value
        return self

    def action1(self, value: str = _ACTION1_DEFAULT) -> ECMModelUpsertAsync[_AnyModelT]:
        """Set the action performed when the search returns exactly one result.

        Args:
            value: One of ``"UPDATE"`` (default), ``"INSERT"``, ``"NONE"``.
        """
        self._action1 = value
        return self

    def action_multiple(self, value: str = _ACTIONM_DEFAULT) -> ECMModelUpsertAsync[_AnyModelT]:
        """Set the action performed when the search returns more than one result.

        Args:
            value: One of ``"ERROR"`` (default), ``"UPDATE"``, ``"INSERT"``, ``"NONE"``.
        """
        self._action_multiple = value
        return self

    def files(self, value: "list[JobRequestFile]", replace: bool = True) -> ECMModelUpsertAsync[_AnyModelT]:
        """Attach files to the upsert operation (only valid for ``ECMDocumentModel``).

        Args:
            value: List of files to attach to the document.
            replace: If ``True`` (default), uploaded files replace existing ones
                (``REPLACEFILES=1``). If ``False``, files are appended.

        Raises:
            TypeError: If the model is not an ``ECMDocumentModel``.
        """
        from ecmind_blue_client.ecm.model._ecm_model import (
            ECMDocumentModel,  # pylint: disable=import-outside-toplevel
        )

        if not isinstance(self._model, ECMDocumentModel):
            raise TypeError(f"files can only be provided for ECMDocumentModel instances, got {type(self._model).__name__}")
        self._files = value
        self._replace_files = replace
        return self

    async def execute(self) -> tuple[int, int, int, str]:
        """Execute the upsert and return the server's response.

        Returns:
            A ``(object_id, object_type_id, hits, action)`` tuple where:
            - ``object_id``: Numeric ID of the inserted or updated object (``-1`` on error).
            - ``object_type_id``: Numeric object type ID (``-1`` on error).
            - ``hits``: Number of objects found by the server-side search.
            - ``action``: Action actually performed — one of ``"UPDATE"``, ``"INSERT"``,
              ``"NONE"``, ``"ERROR"``.
        """
        from ecmind_blue_client.ecm.model._ecm_model import (
            ECMDocumentModel,  # pylint: disable=import-outside-toplevel
        )

        if self._check_mandatory:
            self._ecm._check_required_fields(self._model)  # pylint: disable=protected-access
        xml = self._ecm._build_upsert_xml(  # pylint: disable=protected-access
            self._model, self._search_conditions, folder_id=self._folder_id, register_id=self._register_id
        )
        options: list[str] = []
        if self._replace_files and isinstance(self._model, ECMDocumentModel):
            options.append("REPLACEFILES=1")
        if not self._check_mandatory:
            options.append("CHECKOBLIGATION=0")
        kwargs: dict = {
            "Flags": 0,
            "XML": xml,
            "Action0": self._action0,
            "Action1": self._action1,
            "ActionM": self._action_multiple,
        }
        if options:
            kwargs["Options"] = ";".join(options)
        result = await self._ecm.execute(Jobs.DMS_XMLIMPORT, files=self._files, **kwargs)
        return (
            result.get("ObjectID", int, -1),
            result.get("ObjectType", int, -1),
            result.get("Hits", int, 0),
            result.get("Action", str, ""),
        )
