"""Synchronous typed model query builder for the ECM API."""

from __future__ import annotations

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

from ecmind_blue_client.ecm.builder import DmsQueryBuilderHol
from ecmind_blue_client.ecm.builder._common import DmsQueryFieldSchemaT, DmsQueryObjectSchemaT
from ecmind_blue_client.ecm.model._ecm_model_field import (
    ECMCondition,
    ECMConditionGroup,
    ECMField,
    ECMSortOrder,
)
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._ecm_hol_spec import ECMChildSpec, ECMParentSpec
    from ecmind_blue_client.ecm.model.synchron._ecm_model_hol_query_sync import ECMModelQueryHolSync

# Bound to the private base so users see the concrete model type preserved through query methods.
_AnyModelT = TypeVar("_AnyModelT", bound="_ECMModelBase")
_P1T = TypeVar("_P1T", bound="_ECMModelBase")  # Type of the outermost parent spec in with_parents().


def _apply_condition(target: Any, condition: ECMCondition | ECMConditionGroup) -> None:
    """Apply a condition or condition group to a builder target via duck typing.

    Args:
        target: ``DmsQueryConditionObject`` or ``DmsQueryObjectConditionGroup``.
        condition: Condition or group to apply.
    """
    if isinstance(condition, ECMCondition):
        target.field_condition(condition.field_name, condition.value, condition.operator)
    elif isinstance(condition, ECMConditionGroup):
        _apply_condition_group(target, condition)


def _apply_condition_group(target: Any, group: ECMConditionGroup) -> None:
    """Translate an ``ECMConditionGroup`` into builder calls.

    Args:
        target: ``DmsQueryConditionObject`` or ``DmsQueryObjectConditionGroup``.
        group: Condition group to translate.
    """
    if group.operator == "AND":
        for sub in group.conditions:
            _apply_condition(target, sub)
    elif group.operator == "OR":
        fg = target.field_group("OR")
        for sub in group.conditions:
            if isinstance(sub, ECMCondition):
                fg.field_condition(sub.field_name, sub.value, sub.operator, "OR")
            else:
                # Flatten nested groups within a field_group (schema limitation).
                for s in sub.conditions:
                    if isinstance(s, ECMCondition):
                        fg.field_condition(s.field_name, s.value, s.operator, sub.operator)


def _condition_object_type(
    condition: ECMCondition | ECMConditionGroup,
    primary_identifier: str | int,
) -> str | int:
    """Return the object type identifier for a condition, falling back to the primary model.

    Args:
        condition: Condition or group to classify.
        primary_identifier: ECM identifier of the primary query model.
    """
    if isinstance(condition, ECMCondition) and condition.object_type_identifier is not None:
        return condition.object_type_identifier
    return primary_identifier


class ECMModelQuerySync(Generic[_AnyModelT]):
    """Synchronous query builder for declarative ECM model classes.

    Do not instantiate directly — use ``ECMSync.select()``.

    Args:
        ecm: ``ECMSync`` instance used to execute the query.
        model_class: Model class to query and instantiate results for.
    """

    def __init__(self, ecm: Any, model_class: type[_AnyModelT]) -> None:
        self._ecm = ecm
        self._model_class = model_class
        self._conditions: list[ECMCondition | ECMConditionGroup] = []
        self._sort_orders: list[ECMSortOrder] = []
        self._pagesize: int = 1000
        self._offset: int = 0
        self._maxhits: Optional[int] = None
        self._field_schema: DmsQueryFieldSchemaT = "ALL"
        self._selected_fields: list[str] | None = None
        self._request_rights: bool = False
        self._request_base_params: bool = False
        self._request_file_properties: bool = False
        self._request_variants: bool = False
        self._request_icons: bool = False
        self._request_remarks: bool = False
        self._garbage_mode: bool = False

    def where(self, *conditions: ECMCondition | ECMConditionGroup) -> ECMModelQuerySync[_AnyModelT]:
        """Add filter conditions. Multiple arguments are combined with AND.

        Args:
            *conditions: ``ECMCondition`` or ``ECMConditionGroup`` instances.
        """
        self._conditions.extend(conditions)
        return self

    def pagesize(self, value: int) -> ECMModelQuerySync[_AnyModelT]:
        """Set results per server request (default: 1000).

        Args:
            value: Page size.
        """
        self._pagesize = value
        return self

    def offset(self, value: int) -> ECMModelQuerySync[_AnyModelT]:
        """Set the zero-based start offset.

        Args:
            value: Number of results to skip.
        """
        self._offset = value
        return self

    def limit(self, value: int) -> ECMModelQuerySync[_AnyModelT]:
        """Limit the total number of results across all pages.

        Args:
            value: Maximum result count.
        """
        self._maxhits = value
        return self

    def rights(self, value: bool = True) -> ECMModelQuerySync[_AnyModelT]:
        """Request access rights data from the server (populates ``obj.rights``).

        Args:
            value: ``True`` to enable (default), ``False`` to disable.
        """
        self._request_rights = value
        return self

    def base_params(self, value: bool = True) -> ECMModelQuerySync[_AnyModelT]:
        """Request audit metadata from the server (populates ``obj.base_params``).

        Args:
            value: ``True`` to enable (default), ``False`` to disable.
        """
        self._request_base_params = value
        return self

    def order_by(self, *sort_orders: ECMSortOrder) -> ECMModelQuerySync[_AnyModelT]:
        """Set the sort order for query results.

        Each argument is an ``ECMSortOrder`` created via ``ECMField.ASC`` or ``ECMField.DESC``
        at class level. The position in the argument list determines the sort priority
        (first argument = primary sort key).

        Calling ``order_by()`` multiple times replaces the previous sort specification.

        Args:
            *sort_orders: Sort directives in priority order.

        Example:
            ::

                ecm.select(Invoice).order_by(Invoice.Date.DESC, Invoice.Title.ASC).execute()
        """
        self._sort_orders = list(sort_orders)
        return self

    def file_properties(self, value: bool = True) -> ECMModelQuerySync[_AnyModelT]:
        """Request file properties from the server (populates ``obj.file_properties`` on documents).

        Args:
            value: ``True`` to enable (default), ``False`` to disable.
        """
        self._request_file_properties = value
        return self

    def variants(self, value: bool = True) -> ECMModelQuerySync[_AnyModelT]:
        """Request document variant data from the server (populates ``obj.variants`` on documents).

        Args:
            value: ``True`` to enable (default), ``False`` to disable.
        """
        self._request_variants = value
        return self

    def icons(self, value: bool = True) -> ECMModelQuerySync[_AnyModelT]:
        """Request icon IDs from the server for the returned objects.

        Args:
            value: ``True`` to enable (default), ``False`` to disable.
        """
        self._request_icons = value
        return self

    def remarks(self, value: bool = True) -> ECMModelQuerySync[_AnyModelT]:
        """Request remarks for the returned objects.

        Args:
            value: ``True`` to enable (default), ``False`` to disable.
        """
        self._request_remarks = value
        return self

    def garbage_mode(self, value: bool = True) -> ECMModelQuerySync[_AnyModelT]:
        """Restrict the query to objects in the recycle bin (``GarbageMode=1``).

        When enabled, only deleted objects are returned. When disabled (default),
        only non-deleted objects are returned.

        Args:
            value: ``True`` to search the recycle bin (default), ``False`` to search normally.
        """
        self._garbage_mode = value
        return self

    def fields(self, *fields: "str | ECMField[Any]") -> ECMModelQuerySync[_AnyModelT]:
        """Restrict the returned fields to an explicit list (sets ``field_schema="MIN"``).

        Only effective when the query is executed via ``dms.GetResultList`` (i.e. always
        for normal ``select()`` queries, and for ``get()`` / ``insert_and_get()`` /
        ``update_and_get()`` when ``use_result_list=True``).

        System fields declared on the model (``_ecm_system_fields_``) and sort fields are
        always included regardless of this list.

        Calling this method without arguments resets the field selection (all fields are
        returned again).

        Args:
            *fields: Field names as strings or ``ECMField`` descriptors at class level
                (e.g. ``MyFolder.Title``). Passing no arguments clears any previous
                selection and reverts to ``field_schema="ALL"``.

        Example:
            ::

                ecm.dms.select(Invoice).fields(Invoice.Title, Invoice.Year).execute()
                ecm.dms.get(Invoice, 42, use_result_list=True, fields=[Invoice.Title])
        """
        if not fields:
            self._selected_fields = None
            return self
        self._selected_fields = [  # pylint: disable=protected-access
            name
            for f in fields
            if (name := f._internal_name if isinstance(f, ECMField) else f) is not None
        ]
        return self

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

        Returns a new ``ECMModelQueryHolSync`` that inherits all settings of this query
        and adds ``<ChildObjects>`` support. Call ``limit()`` on the returned builder
        before executing — paging is not supported for hierarchical HOL responses.

        Args:
            *specs: ``ECMChildSpec`` instances defining which child types to include.
                Pass none to include all child types (``child_schema="ALL"``).
            export_depth: Depth of child hierarchy to export (default: ``1`` = direct children).
            child_schema: Object schema for ``<ChildObjects>``. Overridden to ``"ALL"``
                automatically when no specs are given.

        Returns:
            A new ``ECMModelQueryHolSync`` configured with the child settings.
        """
        from ecmind_blue_client.ecm.model.synchron._ecm_model_hol_query_sync import (  # pylint: disable=import-outside-toplevel
            ECMModelQueryHolSync,
        )

        return ECMModelQueryHolSync(self._ecm, self).with_children(*specs, export_depth=export_depth, child_schema=child_schema)

    @overload
    def with_parents(  # type: ignore[misc]
        self,
        __first: "ECMParentSpec[_P1T]",
        *rest: "ECMParentSpec[Any]",
        parent_schema: DmsQueryObjectSchemaT = ...,
    ) -> "ECMModelQueryHolSync[_P1T]": ...

    @overload
    def with_parents(
        self,
        *specs: "ECMParentSpec[Any]",
        parent_schema: DmsQueryObjectSchemaT = ...,
    ) -> "ECMModelQueryHolSync[_ECMModelBase]": ...

    def with_parents(
        self,
        *specs: "ECMParentSpec[Any]",
        parent_schema: DmsQueryObjectSchemaT = "DEF",
    ) -> "ECMModelQueryHolSync[Any]":
        """Switch to a HOL query that includes parent objects in the result.

        Returns a new ``ECMModelQueryHolSync`` whose generic type is the **outermost**
        parent (first spec). The server response is inverted — ``result.main`` is the
        outermost parent and the originally queried objects are accessible via
        ``result.children_of(ECMChildSpec(QueriedModel))``.

        ``limit()`` must be called on the returned builder before executing.

        Args:
            *specs: ``ECMParentSpec`` instances ordered from outermost to innermost
                parent (e.g. ``ECMParentSpec(FolderModel)`` first).
            parent_schema: Object schema for ``<ParentObjects>`` (default: ``"DEF"``).

        Returns:
            A new ``ECMModelQueryHolSync[OutermostParentType]``.
        """
        from ecmind_blue_client.ecm.model.synchron._ecm_model_hol_query_sync import (  # pylint: disable=import-outside-toplevel
            ECMModelQueryHolSync,
        )

        return ECMModelQueryHolSync(self._ecm, self).with_parents(*specs, parent_schema=parent_schema)

    def _build_query(self) -> DmsQueryBuilderHol:
        """Build and return the configured ``DmsQueryBuilderHol``.

        Conditions are bucketed by ``object_type_identifier`` into separate
        ``ConditionObject`` elements, enabling cross-model filtering in one HOL query.
        """
        identifier = self._model_class._ecm_identifier()  # pylint: disable=protected-access
        sys_fields = self._model_class._ecm_system_fields_  # pylint: disable=protected-access
        builder = DmsQueryBuilderHol()
        if self._request_rights:
            builder.rights = True
        if self._request_base_params:
            builder.baseparams = True
        if self._request_file_properties:
            builder.fileinfo = True
        if self._request_variants:
            builder.variants = True
        if self._request_icons:
            builder.icons = True
        if self._request_remarks:
            builder.remarks = True
        schema: DmsQueryFieldSchemaT = "MIN" if self._selected_fields is not None else self._field_schema
        ot = builder.object_type(identifier)
        with ot.fields(schema) as f:
            for sys_field in sys_fields:
                f.field(sys_field)
            if self._selected_fields is not None:
                for field_name in self._selected_fields:
                    f.field(field_name)
            for pos, sort_order in enumerate(self._sort_orders, start=1):
                f.field(sort_order.field_name, sortpos=pos, sortorder=sort_order.direction)
        if self._conditions:
            buckets: dict[str | int, list[ECMCondition | ECMConditionGroup]] = {}
            for condition in self._conditions:
                key = _condition_object_type(condition, identifier)
                buckets.setdefault(key, []).append(condition)
            for ot_id, grouped in buckets.items():
                co = ot.condition_object(ot_id)
                for condition in grouped:
                    _apply_condition(co, condition)
        return builder

    def stream(self) -> Generator[_AnyModelT, None, None]:
        """Execute the query and yield typed model instances. Handles pagination automatically.

        Yields:
            Model instances populated with field values from the server response.
        """
        builder = self._build_query()
        builder.pagesize = self._pagesize
        builder.offset = self._offset
        if self._maxhits is not None:
            builder.maxhits = self._maxhits

        total_hits = -1

        while total_hits == -1 or builder.offset < total_hits:
            result = self._ecm.execute(
                "dms.GetResultList",
                Flags=16,
                XML=builder.to_xml(),
                **{"GarbageMode": 1} if self._garbage_mode else {},
            )

            total_hits = result.get("TotalHits", int, 0)
            builder.offset += result.get("Count", int)
            dms_content = DmsContentParserHol(result.files[0])

            for ot in dms_content.object_types():
                for obj in ot.objects():
                    yield self._model_class._from_dms_parsed(ot, obj)  # type: ignore[return-value]  # pylint: disable=protected-access

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

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