"""Shared helpers for building and parsing HOL queries with children and parents."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from ecmind_blue_client.ecm.parser._dms_content_object_hol import DmsContentObjectHol
    from ecmind_blue_client.ecm.parser._dms_content_object_type_hol import DmsContentObjectTypeHol
    from ecmind_blue_client.ecm.model._ecm_model import _ECMModelBase
    from ecmind_blue_client.ecm.model._ecm_hol_spec import ECMChildSpec, ECMParentSpec

# ECM maintype constants for generic model instantiation.
_MAINTYPE_FOLDER = 0
_MAINTYPE_DOCUMENT = 4


# ---------------------------------------------------------------------------
# Generic model instantiation
# ---------------------------------------------------------------------------


def _instantiate_generic(ot: DmsContentObjectTypeHol, obj: DmsContentObjectHol) -> _ECMModelBase:
    """Instantiate a generic model from a parser ObjectType/Object pair.

    Selects the appropriate base model class (folder, register, or document) based on
    the ``maintype`` attribute of the ObjectType, then creates an instance via
    ``_from_dms_parsed``.

    Args:
        ot: Parsed HOL ``<ObjectType>`` element.
        obj: Parsed HOL ``<Object>`` element.

    Returns:
        A ``_ECMModelBase`` subclass instance populated with field values.
    """
    from ecmind_blue_client.ecm.model._ecm_model import (  # pylint: disable=import-outside-toplevel
        make_document_model,
        make_folder_model,
        make_register_model,
    )

    internal = ot.internal_name or str(ot.id)
    maintype = ot.maintype
    if maintype == _MAINTYPE_FOLDER:
        cls = make_folder_model(internal)
    elif maintype == _MAINTYPE_DOCUMENT:
        cls = make_document_model(internal)
    else:
        cls = make_register_model(internal)
    return cls._from_dms_parsed(ot, obj)  # type: ignore[return-value]  # pylint: disable=protected-access


def _instantiate_from_spec(
    spec: ECMParentSpec[Any] | ECMChildSpec[Any],
    ot: DmsContentObjectTypeHol,
    obj: DmsContentObjectHol,
) -> _ECMModelBase:
    """Instantiate a model instance using a spec's model class or generically.

    If the spec's ``model`` is a typed model class, uses that class's
    ``_from_dms_parsed`` classmethod. Otherwise falls back to ``_instantiate_generic``.

    Args:
        spec: ``ECMParentSpec`` or ``ECMChildSpec`` whose ``model`` field is consulted.
        ot: Parsed HOL ``<ObjectType>`` element.
        obj: Parsed HOL ``<Object>`` element.

    Returns:
        A model instance.
    """
    if isinstance(spec.model, (str, int)):
        return _instantiate_generic(ot, obj)
    return spec.model._from_dms_parsed(ot, obj)  # type: ignore[return-value]  # pylint: disable=protected-access


# ---------------------------------------------------------------------------
# Recursive descendant collection
# ---------------------------------------------------------------------------


def _collect_descendants(
    obj: DmsContentObjectHol,
    child_specs: list[ECMChildSpec[Any]],
) -> dict[str | int, list[_ECMModelBase]]:
    """Recursively collect all descendant objects at any nesting depth.

    Unlike ``_collect_children`` (which only looks at direct children), this function
    traverses the full nested ``<ChildObjects>`` tree and collects all objects whose
    type matches a spec in ``child_specs``.

    Used when ``with_parents()`` is active: the top-level response node is the outermost
    parent and the queried objects may be nested several levels deep. All matching types
    are flattened into a single dict.

    When ``child_specs`` is empty, every descendant at every level is collected generically.

    Both the ``internal_name`` (str) and ``id`` (int) of each collected type are stored as
    keys so that callers can look up by either form.

    Args:
        obj: The top-level HOL ``<Object>`` element to start from.
        child_specs: Specs defining which descendant types to collect (and how to type them).
            Pass an empty list to collect all descendants generically.

    Returns:
        Dict mapping ``internal_name`` (str) and ``id`` (int) → flat list of model instances
        collected across all nesting levels.
    """
    spec_by_str: dict[str, ECMChildSpec[Any]] = {}
    spec_by_int: dict[int, ECMChildSpec[Any]] = {}
    for spec in child_specs:
        spec_id = spec.identifier()
        if isinstance(spec_id, str):
            spec_by_str[spec_id] = spec
        else:
            spec_by_int[spec_id] = spec

    result: dict[str | int, list[_ECMModelBase]] = {}
    _descend_collect(obj, child_specs, spec_by_str, spec_by_int, result)
    return result


def _descend_collect(
    obj: DmsContentObjectHol,
    child_specs: list[ECMChildSpec[Any]],
    spec_by_str: dict[str, ECMChildSpec[Any]],
    spec_by_int: dict[int, ECMChildSpec[Any]],
    result: dict[str | int, list[_ECMModelBase]],
) -> None:
    """Recursive helper for ``_collect_descendants``.

    Args:
        obj: Current ``<Object>`` node to inspect.
        child_specs: Full list of specs (used to decide whether filtering is active).
        spec_by_str: Specs keyed by internal name for fast lookup.
        spec_by_int: Specs keyed by type ID for fast lookup.
        result: Accumulator dict mutated in place.
    """
    for child_ot in obj.child_object_types():
        child_internal = child_ot.internal_name
        child_type_id = child_ot.id
        spec = spec_by_str.get(child_internal) or spec_by_int.get(child_type_id)

        if child_specs and spec is None:
            # Not a target type — skip collection but keep descending to find deeper targets.
            for child_obj in child_ot.objects():
                _descend_collect(child_obj, child_specs, spec_by_str, spec_by_int, result)
        else:
            for child_obj in child_ot.objects():
                instance = (
                    _instantiate_from_spec(spec, child_ot, child_obj)
                    if spec is not None
                    else _instantiate_generic(child_ot, child_obj)
                )
                if child_internal:
                    result.setdefault(child_internal, []).append(instance)
                result.setdefault(child_type_id, []).append(instance)
                # Recurse into this child to find further nested targets.
                _descend_collect(child_obj, child_specs, spec_by_str, spec_by_int, result)


# ---------------------------------------------------------------------------
# Direct children collection (used by with_children() without parents)
# ---------------------------------------------------------------------------


def _collect_children(
    main_obj: DmsContentObjectHol,
    child_specs: list[ECMChildSpec[Any]],
) -> dict[str | int, list[_ECMModelBase]]:
    """Collect direct child objects from a HOL main object.

    Only inspects the immediate ``<ChildObjects>/<ObjectType>`` elements inside
    ``main_obj`` — no recursive descent. Use ``_collect_descendants`` when children
    may be nested at arbitrary depth (e.g. when ``with_parents()`` is active).

    When ``child_specs`` is non-empty, only child types that match at least one spec are
    included and typed accordingly; when empty, all child types are included generically.

    Both the ``internal_name`` (str) and ``id`` (int) of each child type are stored as
    keys in the returned dict so that callers can look up by either form.

    Args:
        main_obj: The HOL main ``<Object>`` element (queried object).
        child_specs: Optional list of specs restricting which types are included.

    Returns:
        Dict mapping ``internal_name`` and ``id`` → list of model instances.
    """
    # Build a lookup from identifier → spec for fast matching.
    spec_by_str: dict[str, ECMChildSpec[Any]] = {}
    spec_by_int: dict[int, ECMChildSpec[Any]] = {}
    for spec in child_specs:
        spec_id = spec.identifier()
        if isinstance(spec_id, str):
            spec_by_str[spec_id] = spec
        else:
            spec_by_int[spec_id] = spec

    children: dict[str | int, list[_ECMModelBase]] = {}

    for child_ot in main_obj.child_object_types():
        child_internal = child_ot.internal_name
        child_type_id = child_ot.id

        # Determine the matching spec (if any).
        spec: ECMChildSpec[Any] | None = spec_by_str.get(child_internal) or spec_by_int.get(child_type_id)

        # When specs are given, skip types that don't match any spec.
        if child_specs and spec is None:
            continue

        instances: list[_ECMModelBase] = []
        for child_obj in child_ot.objects():
            if spec is not None:
                instances.append(_instantiate_from_spec(spec, child_ot, child_obj))
            else:
                instances.append(_instantiate_generic(child_ot, child_obj))

        # Store under both keys so callers can look up by either form.
        if child_internal:
            children[child_internal] = instances
        children[child_type_id] = instances

    return children


# ---------------------------------------------------------------------------
# Query builder extension
# ---------------------------------------------------------------------------


def extend_builder_with_hol(  # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
    builder_ot: Any,
    main_identifier: str | int,
    child_specs: list[ECMChildSpec[Any]],
    parent_specs: list[ECMParentSpec[Any]],
    export_depth: int,
    child_schema: str,
    parent_schema: str,
) -> None:
    """Attach ``<ChildObjects>`` and/or ``<ParentObjects>`` to a HOL query ObjectType.

    Modifies ``builder_ot`` (a ``DmsQueryObjectTypeHol`` instance) in place by adding
    the child/parent sub-elements to the existing ``<ObjectType>`` node.

    Args:
        builder_ot: ``DmsQueryObjectTypeHol`` whose XML node is extended.
        main_identifier: Identifier of the queried main type — used as ``<SubObjectType>``.
        child_specs: Specs for child types; empty means all child types (``child_schema="ALL"``).
        parent_specs: Specs for parent types; empty means no parent traversal.
        export_depth: ``export_depth`` attribute on ``<ChildObjects>``.
        child_schema: ``child_schema`` attribute on ``<ChildObjects>``.
        parent_schema: ``parent_schema`` attribute on ``<ParentObjects>``.
    """
    from ecmind_blue_client.ecm.model._ecm_model_field import ECMCondition, ECMConditionGroup  # pylint: disable=import-outside-toplevel

    if child_specs or export_depth > 0 or child_schema == "ALL":
        effective_schema = "ALL" if not child_specs else child_schema
        field_schema = "ALL" if not child_specs else None
        child_objs = builder_ot.child_objects(
            child_schema=effective_schema,
            export_depth=export_depth,
            field_schema=field_schema,
        )
        for spec in child_specs:
            cot = child_objs.child_object_type(spec.identifier())
            cot.fields(spec.field_schema)
            for condition in spec._conditions:  # pylint: disable=protected-access
                if isinstance(condition, ECMCondition):
                    cot.field_condition(condition.field_name, condition.value, condition.operator)
                elif isinstance(condition, ECMConditionGroup):
                    _apply_condition_group_to_cot(cot, condition)

    if parent_specs:
        parent_objs = builder_ot.parent_objects(parent_schema=parent_schema, field_schema="ALL")
        for spec in parent_specs:
            pot = parent_objs.parent_object_type(spec.identifier())
            pot.fields(spec.field_schema)
        parent_objs.sub_object_type(main_identifier)


def _apply_condition_group_to_cot(cot: Any, group: Any) -> None:
    """Translate an ``ECMConditionGroup`` into ``field_condition`` calls on a ChildObjectType.

    Args:
        cot: ``DmsQueryChildObjectType`` target.
        group: ``ECMConditionGroup`` to translate.
    """
    from ecmind_blue_client.ecm.model._ecm_model_field import ECMCondition, ECMConditionGroup  # pylint: disable=import-outside-toplevel

    if group.operator == "AND":
        for sub in group.conditions:
            if isinstance(sub, ECMCondition):
                cot.field_condition(sub.field_name, sub.value, sub.operator)
            elif isinstance(sub, ECMConditionGroup):
                _apply_condition_group_to_cot(cot, sub)
    elif group.operator == "OR":
        for sub in group.conditions:
            if isinstance(sub, ECMCondition):
                cot.field_condition(sub.field_name, sub.value, sub.operator, "OR")
