"""Synchronous HOL query builder supporting child and parent objects."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Generator, Generic, TypeVar

from ecmind_blue_client.ecm.builder._common import DmsQueryObjectSchemaT
from ecmind_blue_client.ecm.model._ecm_hol_helpers import (
    _collect_children,
    _collect_descendants,
    _instantiate_generic,
    extend_builder_with_hol,
)
from ecmind_blue_client.ecm.model._ecm_hol_result import ECMHolResult
from ecmind_blue_client.ecm.model._ecm_hol_spec import ECMChildSpec, ECMParentSpec
from ecmind_blue_client.ecm.parser import DmsContentParserHol

if TYPE_CHECKING:
    from ecmind_blue_client.ecm.model._ecm_model import _ECMModelBase
    from ecmind_blue_client.ecm.model.synchron._ecm_model_query_sync import ECMModelQuerySync

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


class ECMModelQueryHolSync(Generic[_AnyModelT]):  # pylint: disable=too-many-instance-attributes
    """Synchronous HOL query builder that fetches children and/or parent objects.

    Do not instantiate directly — call ``with_children()`` or ``with_parents()`` on an
    ``ECMModelQuerySync`` instance.

    **Children (``with_children()``):**
    Paging is not supported when children are active. ``limit()`` **must** be called
    before ``execute()`` / ``stream()``.

    **Parents (``with_parents()``):**
    The server response is structured from the **outermost parent** down. When parents are
    requested the generic type parameter ``_AnyModelT`` represents the outermost parent
    (first spec passed to ``with_parents()``), and the originally queried objects are
    accessible as descendants via ``result.children_of(ECMChildSpec(QueriedModel))``.

    Example (children only)::

        results = (
            ecm.dms.select(Register)
            .where(Register.Name == "Subreg1")
            .with_children(ECMChildSpec(Document), export_depth=1)
            .limit(50)
            .execute()
        )
        for r in results:
            docs = r.children_of(ECMChildSpec(Document))

    Example (parents)::

        results = (
            ecm.dms.select(Document)
            .where(Document.Name == "Invoice")
            .with_parents(ECMParentSpec(FolderModel), ECMParentSpec(RegisterModel))
            .limit(50)
            .execute()
        )
        for r in results:
            folder   = r.main                                   # FolderModel
            register = r.children_of(ECMChildSpec(RegisterModel))[0]
            doc      = r.children_of(ECMChildSpec(Document))[0]

    Args:
        ecm: The ``ECMSync`` instance used to execute requests.
        base_query: The base ``ECMModelQuerySync`` instance whose settings are inherited.
    """

    def __init__(self, ecm: Any, base_query: ECMModelQuerySync[_AnyModelT]) -> None:
        self._ecm = ecm
        self._base_query = base_query
        self._child_specs: list[ECMChildSpec[Any]] = []
        self._parent_specs: list[ECMParentSpec[Any]] = []
        self._export_depth: int = 1
        self._child_schema: DmsQueryObjectSchemaT = "DEF"
        self._parent_schema: DmsQueryObjectSchemaT = "DEF"
        self._maxhits: int | None = None

    def with_children(
        self,
        *specs: ECMChildSpec[Any],
        export_depth: int = 1,
        child_schema: DmsQueryObjectSchemaT = "DEF",
    ) -> ECMModelQueryHolSync[_AnyModelT]:
        """Include child objects in the query result.

        Generates a ``<ChildObjects>`` element in the request XML. When ``specs`` are
        provided, only the listed child types are requested (``child_schema="DEF"``).
        When no specs are given, all child types are returned (``child_schema="ALL"``).

        ``export_depth`` controls the hierarchy depth relative to the queried object:
        ``1`` = direct children only, ``2`` = children and grandchildren, etc.

        Args:
            *specs: ``ECMChildSpec`` instances defining which child types to include
                and how to type them. Pass none to include all child types generically.
            export_depth: Hierarchy depth to export. Must be ``>= 1`` to receive any
                children. Defaults to ``1``.
            child_schema: Object schema for the ``<ChildObjects>`` element. Ignored when
                ``specs`` is empty (``"ALL"`` is used automatically). Defaults to ``"DEF"``.

        Returns:
            ``self`` for chaining.
        """
        self._child_specs = list(specs)
        self._export_depth = export_depth
        self._child_schema = child_schema if specs else "ALL"
        return self

    def with_parents(
        self,
        *specs: ECMParentSpec[Any],
        parent_schema: DmsQueryObjectSchemaT = "DEF",
    ) -> ECMModelQueryHolSync[_AnyModelT]:
        """Include parent objects in the query result.

        Generates a ``<ParentObjects>`` element in the request XML. When parents are
        requested the server response is **inverted** — the outermost parent becomes the
        top-level ``<ObjectType>`` and is returned as ``result.main``.

        The originally queried objects are nested inside the response and are accessible
        via ``result.children_of(ECMChildSpec(QueriedModel))``. All intermediate ancestor
        types listed in ``specs`` are also accessible the same way.

        Args:
            *specs: ``ECMParentSpec`` instances ordered from **outermost to innermost**
                parent (e.g. Folder first, then Register).
            parent_schema: Object schema for the ``<ParentObjects>`` element. Defaults to ``"DEF"``.

        Returns:
            ``self`` for chaining.
        """
        self._parent_specs = list(specs)
        self._parent_schema = parent_schema
        return self

    def limit(self, value: int) -> ECMModelQueryHolSync[_AnyModelT]:
        """Set the maximum number of results to return.

        Required when children or parents are active because the server cannot paginate
        HOL responses with hierarchy. The limit is sent as ``maxhits`` in the request.

        Args:
            value: Maximum number of main objects to return.

        Returns:
            ``self`` for chaining.
        """
        self._maxhits = value
        return self

    def stream(self) -> Generator[ECMHolResult[_AnyModelT], None, None]:
        """Execute the query and yield ``ECMHolResult`` instances.

        Each yielded result wraps one top-level object (the queried model when no parents
        are active; the outermost parent when ``with_parents()`` was called) together with
        its collected children.

        Yields:
            ``ECMHolResult`` instances, one per top-level object in the server response.

        Raises:
            ValueError: If ``limit()`` was not called before streaming. A limit is
                required because the server cannot paginate hierarchical HOL responses.
        """
        if self._maxhits is None:
            raise ValueError(
                "limit() must be called before stream() / execute() when using with_children() or with_parents(). "
                "The server cannot paginate hierarchical HOL responses."
            )

        model_class = self._base_query._model_class  # pylint: disable=protected-access
        main_identifier = model_class._ecm_identifier()  # pylint: disable=protected-access

        builder = self._base_query._build_query()  # pylint: disable=protected-access
        builder.maxhits = self._maxhits

        ot = builder.object_type(main_identifier)
        extend_builder_with_hol(
            builder_ot=ot,
            main_identifier=main_identifier,
            child_specs=self._child_specs,
            parent_specs=self._parent_specs,
            export_depth=self._export_depth,
            child_schema=self._child_schema,
            parent_schema=self._parent_schema,
        )

        garbage_mode = self._base_query._garbage_mode  # pylint: disable=protected-access
        result = self._ecm.execute(
            "dms.GetResultList",
            Flags=16,
            XML=builder.to_xml(),
            **{"GarbageMode": 1} if garbage_mode else {},
        )

        dms_content = DmsContentParserHol(result.files[0])

        if self._parent_specs:
            # Response is inverted: outermost parent is the top-level node.
            # Instantiate it as main; collect the queried objects and all other
            # listed ancestor types as descendants via recursive traversal.
            # Include intermediate parent types (specs[1:]) so they are accessible
            # via result.children_of() alongside the queried objects.
            first_spec = self._parent_specs[0]
            all_descendant_specs = [
                *self._child_specs,
                *[ECMChildSpec(spec.model) for spec in self._parent_specs[1:]],
                ECMChildSpec(model_class),
            ]

            for top_ot in dms_content.object_types():
                for top_obj in top_ot.objects():
                    if isinstance(first_spec.model, type):
                        main_instance = first_spec.model._from_dms_parsed(top_ot, top_obj)  # type: ignore[return-value]  # pylint: disable=protected-access
                    else:
                        main_instance = _instantiate_generic(top_ot, top_obj)
                    children = _collect_descendants(top_obj, all_descendant_specs)
                    yield ECMHolResult(main_instance, children)  # type: ignore[misc]
        else:
            # No parents: main object is at the top level, direct children only.
            for top_ot in dms_content.object_types():
                for top_obj in top_ot.objects():
                    main_instance = model_class._from_dms_parsed(top_ot, top_obj)  # type: ignore[return-value]  # pylint: disable=protected-access
                    children = _collect_children(top_obj, self._child_specs)
                    yield ECMHolResult(main_instance, children)  # type: ignore[misc]

    def execute(self) -> list[ECMHolResult[_AnyModelT]]:
        """Execute the query and return all results as a list.

        Returns:
            List of ``ECMHolResult`` instances.
        """
        return list(self.stream())
