"""Shared helpers for isolation fix executors.

The ``_remove_devices_matching`` helper backs every ``strip.*``
executor — they're all "find these elements under ``<devices>``,
remove them, log the count" with a small per-fix predicate. This
keeps each stripper to a one-liner.
"""

from __future__ import annotations

import xml.etree.ElementTree as ET  # noqa: TC003
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Callable

    from mgf.vm.core.common.progress import ProgressReporter


__all__ = ["devices_element", "remove_devices_matching"]


def devices_element(target: ET.ElementTree) -> ET.Element | None:
    """Return ``<domain>/<devices>`` or ``None`` if either is absent.

    Centralises the ``target.getroot().find("./devices")`` narrowing
    so each fix executor is one less mypy ``Item "None"`` complaint.
    A tree without a root or without ``<devices>`` is unusual but
    possible (e.g., a pre-create XML stub) — fixes treat the
    missing element as "already satisfied".
    """
    root = target.getroot()
    if root is None:
        return None
    return root.find("./devices")


def remove_devices_matching(
    target: ET.ElementTree,
    *,
    tag: str,
    predicate: Callable[[ET.Element], bool] | None = None,
    reporter: ProgressReporter,
    dry_run: bool,
    fix_id: str,
) -> int:
    """Remove every ``<devices>/<{tag}>`` matching *predicate*; return count.

    Idempotent: running twice on a satisfied target returns 0 the
    second time and emits ``"already satisfied"`` at the reporter
    (mirrors Phase 15's executor convention).

    ``dry_run=True`` logs the count without mutating the tree.
    """
    devices = devices_element(target)
    if devices is None:
        reporter.report(f"{fix_id}: already satisfied (no <devices> element)")
        return 0
    matches: list[ET.Element] = [
        elem for elem in devices.findall(tag) if predicate is None or predicate(elem)
    ]
    if not matches:
        reporter.report(f"{fix_id}: already satisfied")
        return 0
    if dry_run:
        reporter.report(
            f"{fix_id}: (dry-run) would remove {len(matches)} <{tag}> element(s)",
        )
        return len(matches)
    for elem in matches:
        devices.remove(elem)
    reporter.report(f"{fix_id}: removed {len(matches)} <{tag}> element(s)")
    return len(matches)
