"""Shared helpers for HA / Zigbee2MQTT light target processors.

``HALightTargetProcessor`` and ``Z2MLightTargetProcessor`` historically
duplicated their colour-source swap logic character-for-character — only
the log prefix and a docstring differed (audit finding C5). This module
hosts the deduplicated implementation.

We deliberately stop short of extracting a full ``BaseLightTargetProcessor``
ABC here: the read sites for the per-processor state are spread across ~38
locations per file and a wholesale composition refactor risks regressing
the live LED control loop. The free-function approach below is the
minimum-blast-radius way to delete the duplication. The processor still
owns its state; the helper reaches in to mutate it, which is ugly but
Pythonic and isolated to two methods.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from ledgrab.utils import get_logger

if TYPE_CHECKING:
    # ``LightTargetSwapState`` is a structural protocol — anything carrying
    # the listed attributes is acceptable. We do not import the concrete
    # processor classes to avoid a circular dependency.
    from typing import Any, Protocol

    class LightTargetSwapState(Protocol):
        _is_running: bool
        _css_stream: Any
        _color_stream: Any
        _ctx: Any
        _css_id: str
        _color_vs_id: str
        _source_kind: str
        _target_id: str


logger = get_logger(__name__)


def swap_color_source(
    processor: "LightTargetSwapState",
    new_kind: str,
    new_color_vs_id: str,
    *,
    log_label: str,
) -> None:
    """Release the current colour source and acquire the new one.

    Mirrors what ``HALightTargetProcessor._swap_color_source`` and
    ``Z2MLightTargetProcessor._swap_color_source`` used to do inline.

    The caller is responsible for clearing per-entity history
    (``_previous_colors``, ``_previous_on``) after this returns —
    that state is owned by the processor, not the colour source.

    ``log_label`` is the short identifier used in warning logs
    (e.g. ``"HA light"`` or ``"Z2M light"``) so a failure is
    traceable back to the right processor in mixed deployments.
    """
    # Tear down the previously-acquired stream first to keep ref-counts honest.
    if processor._is_running:
        if processor._css_stream and processor._ctx.color_strip_stream_manager:
            try:
                processor._ctx.color_strip_stream_manager.release(
                    processor._css_id, processor._target_id
                )
            except Exception:
                # Manager-level errors are non-fatal: stream may already be
                # gone if the source was deleted out from under us.
                pass
            processor._css_stream = None
        if processor._color_stream is not None and processor._ctx.value_stream_manager:
            try:
                processor._ctx.value_stream_manager.release(processor._color_vs_id)
            except Exception:
                pass
            processor._color_stream = None

    processor._source_kind = new_kind
    processor._color_vs_id = new_color_vs_id

    if not processor._is_running:
        # Not started yet; the start() path will acquire when called.
        return

    if processor._source_kind == "color_vs":
        if processor._color_vs_id and processor._ctx.value_stream_manager:
            try:
                processor._color_stream = processor._ctx.value_stream_manager.acquire(
                    processor._color_vs_id
                )
            except Exception as e:
                logger.warning(
                    "%s %s: failed to acquire color VS stream: %s",
                    log_label,
                    processor._target_id,
                    e,
                )
    else:
        if processor._css_id and processor._ctx.color_strip_stream_manager:
            try:
                processor._css_stream = processor._ctx.color_strip_stream_manager.acquire(
                    processor._css_id, processor._target_id
                )
            except Exception as e:
                logger.warning(
                    "%s %s: failed to re-acquire CSS stream: %s",
                    log_label,
                    processor._target_id,
                    e,
                )
