# -*- coding: utf-8 -*-
"""Вспомогательные функции выделения / подсветки стоек для 3D-виджета просмотра моделей."""

from __future__ import annotations

from typing import Any, TYPE_CHECKING

from gui.components.model_view._mv_rack_geometry import rack_bbox
from error_logger import log_exception

try:
    import pyvista as pv  # noqa: F401
except ImportError:
    pv = None  # type: ignore[assignment]

if TYPE_CHECKING:
    from gui.components.model_view_widget import ModelViewWidget


class RackSelectionMixin:
    """Вспомогательные функции визуализации выделения, контура и изоляции стоек."""

    def get_selected_rack_entry(self: "ModelViewWidget") -> dict[str, Any] | None:
        rid = str(self._selected_rack_id or "")
        if not rid:
            return None
        for entry in self._rack_entries:
            if str(entry.get("rack_id", "")) == rid:
                return entry
        return None

    def get_selected_rack_display_color(self: "ModelViewWidget") -> str:
        entry = self.get_selected_rack_entry()
        if entry is None:
            return "#59B6E6"
        params = dict(entry.get("params") or {})
        color, _opacity = self._resolve_rack_style(params)
        return str(color or "#59B6E6")

    def rebuild_zone_racks_for_updated_zone(self: "ModelViewWidget", zone_id: str) -> list[str]:
        """Пересоздать акторы стоек/полок для зоны после обновления высоты/объёма зоны."""
        zid = str(zone_id or "")
        if not zid or not self._plotter:
            return []
        updated_rack_ids: list[str] = []
        for idx, entry in enumerate(list(self._rack_entries)):
            if str(entry.get("zone_id") or "") != zid:
                continue
            rack_id = str(entry.get("rack_id") or "")
            if not rack_id:
                continue
            center = entry.get("center") or (0.0, 0.0)
            params = dict(entry.get("params") or {})
            rotation = int(entry.get("rotation", 0)) % 360
            color, opacity = self._resolve_rack_style(params)
            actors, slot_actors = self._spawn_rack_actors(
                float(center[0]), float(center[1]), zid, params, rotation, color, opacity,
            )
            if not actors:
                continue
            for actor in list(entry.get("actors") or []):
                try:
                    self._plotter.remove_actor(actor)
                except Exception as _exc:
                    log_exception(__name__, "rebuild_zone_racks_for_updated_zone", _exc)
            self._clear_slot_shelf_actors(rack_id, clear_slot_id=None)
            hidden_slot_ids = set(str(v) for v in (entry.get("hidden_slot_ids") or set()))
            for sid in hidden_slot_ids:
                slot_actor = slot_actors.get(sid)
                if slot_actor is not None:
                    try:
                        slot_actor.SetVisibility(0)
                    except Exception as _exc:
                        log_exception(__name__, "rebuild_zone_racks_for_updated_zone", _exc)
            entry["bbox"] = rack_bbox(float(center[0]), float(center[1]), params, rotation)
            entry["actors"] = actors
            entry["slot_actors"] = slot_actors
            entry["hidden_slot_ids"] = hidden_slot_ids
            self._apply_rack_helper_visibility_policy(entry, True)
            self._rack_entries[idx] = entry

            previous_target_zone = self._shelf_target_zone_id
            try:
                self._shelf_target_zone_id = zid
                slot_keys = [
                    (entry_rid, slot_id)
                    for (entry_rid, slot_id) in list((self._rack_shelf_params or {}).keys())
                    if str(entry_rid) == rack_id
                ]
                slot_keys.sort(key=lambda key: self._slot_index_from_id(str(key[1])))
                for key in slot_keys:
                    slot_id = str(key[1])
                    payload = dict(self._rack_shelf_params.get(key) or {})
                    if not payload:
                        continue
                    normalized = self._render_shelves_for_slot(rack_id, slot_id, payload)
                    if not normalized:
                        continue
                    self._rack_shelf_params[(rack_id, slot_id)] = dict(normalized)
                    self.set_rack_slot_occupied(rack_id, slot_id, True)
            finally:
                self._shelf_target_zone_id = previous_target_zone

            self._set_rack_shelf_actors_visibility(rack_id, True)
            self._apply_rack_shelf_opacity(rack_id)
            updated_rack_ids.append(rack_id)

        if self._selected_rack_id:
            self._apply_selected_rack_visual(str(self._selected_rack_id))
        self._plotter.update()
        return updated_rack_ids

    def select_rack_by_id(self: "ModelViewWidget", rack_id: str | None, zone_id: str | None = None) -> bool:
        rid = str(rack_id or "")
        zid = str(zone_id or "")
        if not rid:
            self._set_selected_rack(None)
            return False
        for entry in self._rack_entries:
            if str(entry.get("rack_id", "")) != rid:
                continue
            if zid and str(entry.get("zone_id", "")) != zid:
                continue
            self._set_selected_rack(rid)
            return True
        return False

    def clear_selected_rack(self: "ModelViewWidget") -> None:
        """Сбросить визуальное состояние выделенной стойки и контур наведения."""
        self._set_selected_rack(None)
        self._clear_hover_rack_highlight()

    def _set_selected_rack(self: "ModelViewWidget", rack_id: str | None) -> None:
        rid = str(rack_id or "")
        current = str(self._selected_rack_id or "")
        if not rid:
            self._selected_rack_id = None
            self._clear_selected_rack_visual()
            if current:
                self.rack_selected_changed.emit("")
            return
        if current != rid:
            self._selected_rack_id = rid
            self._apply_selected_rack_visual(rid)
            self.rack_selected_changed.emit(rid)
            return
        self._apply_selected_rack_visual(rid)

    def _clear_selected_rack_visual(self: "ModelViewWidget") -> None:
        snapshot = list(getattr(self, "_selected_rack_visual_props", []) or [])
        for actor, opacity, edge_vis, line_width, edge_color in snapshot:
            if actor is None:
                continue
            try:
                prop = actor.GetProperty()
                prop.SetOpacity(float(opacity))
                prop.SetEdgeVisibility(bool(edge_vis))
                prop.SetLineWidth(float(line_width))
                prop.SetEdgeColor(*edge_color)
            except Exception as _exc:
                log_exception(__name__, "_clear_selected_rack_visual", _exc)
        self._selected_rack_visual_props = []
        self._selected_rack_visual_id = None
        self._clear_selected_rack_contour(render=False)
        if self._rack_isolation_active and self._rack_isolation_rack_id:
            self._apply_rack_isolation_visual(str(self._rack_isolation_rack_id))

    def _apply_selected_rack_visual(self: "ModelViewWidget", rack_id: str) -> None:
        rid = str(rack_id or "")
        if not rid:
            self._clear_selected_rack_visual()
            return
        entry = None
        for candidate in self._rack_entries:
            if str(candidate.get("rack_id", "")) == rid:
                entry = candidate
                break
        if entry is None:
            self._clear_selected_rack_visual()
            return
        self._clear_selected_rack_visual()
        slot_actor_ids = {
            id(actor)
            for actor in (entry.get("slot_actors") or {}).values()
            if actor is not None
        }
        snapshot: list[tuple[object, float, bool, float, tuple[float, float, float]]] = []
        isolation_active = bool(
            self._rack_isolation_active
            and str(self._rack_isolation_rack_id or "") == rid
        )
        for idx, actor in enumerate(entry.get("actors", [])):
            if actor is None:
                continue
            try:
                prop = actor.GetProperty()
                if prop is None:
                    continue
                edge_color = tuple(prop.GetEdgeColor())
                snapshot.append(
                    (
                        actor,
                        float(prop.GetOpacity()),
                        bool(prop.GetEdgeVisibility()),
                        float(prop.GetLineWidth()),
                        (float(edge_color[0]), float(edge_color[1]), float(edge_color[2])),
                    )
                )
                if isolation_active:
                    prop.SetEdgeVisibility(False)
                    prop.SetLineWidth(1.0)
                else:
                    prop.SetEdgeVisibility(False)
                    prop.SetLineWidth(1.0)
                if isolation_active and idx > 0:
                    continue
                if id(actor) in slot_actor_ids:
                    if (
                        self._interaction_manager.is_active("shelf_placement")
                        and str(getattr(self, "_shelf_target_rack_id", "") or "") == rid
                    ):
                        # В режиме редактирования полок прокси секций должны оставаться невидимыми.
                        prop.SetOpacity(0.0)
                    else:
                        # Объём между стойками сохраняет свою текущую прозрачность.
                        pass
                elif idx == 0:
                    # Основное тело стойки: полная заливка при выделении.
                    prop.SetOpacity(1.0)
                else:
                    prop.SetOpacity(min(1.0, float(prop.GetOpacity()) + 0.08))
            except Exception as _exc:
                log_exception(__name__, "_apply_selected_rack_visual", _exc)
        self._selected_rack_visual_props = snapshot
        self._selected_rack_visual_id = rid
        self._set_selected_rack_contour(entry)
        if self._plotter:
            self._plotter.update()

    def _set_selected_rack_contour(self: "ModelViewWidget", entry: dict[str, Any]) -> None:
        self._clear_selected_rack_contour(render=False)
        if not self._plotter:
            return
        contour = self._build_rack_hover_contour_mesh(entry)
        if contour is None:
            return
        try:
            self._selected_rack_contour_actor = self._plotter.add_mesh(
                contour,
                color=(0.25, 0.55, 1.0),
                line_width=3.0,
                name="_selected_rack_contour",
                pickable=False,
            )
        except Exception as exc:
            log_exception(__name__, "_set_selected_rack_contour", exc)
            self._selected_rack_contour_actor = None

    def _clear_selected_rack_contour(self: "ModelViewWidget", render: bool = True) -> None:
        actor = getattr(self, "_selected_rack_contour_actor", None)
        if actor is not None and self._plotter is not None:
            try:
                self._plotter.remove_actor(actor)
            except Exception as _exc:
                log_exception(__name__, "_clear_selected_rack_contour", _exc)
        self._selected_rack_contour_actor = None
        if render:
            if hasattr(self, "_safe_render"):
                self._safe_render(min_interval_s=1.0 / 60.0)
            elif self._plotter:
                self._plotter.update()

    def _clear_rack_isolation_visual(self: "ModelViewWidget") -> None:
        snapshot = list(getattr(self, "_rack_isolation_visual_props", []) or [])
        for actor, visibility, opacity, edge_vis, line_width, edge_color in snapshot:
            if actor is None:
                continue
            try:
                actor.SetVisibility(int(visibility))
                prop = actor.GetProperty()
                if prop is None:
                    continue
                prop.SetOpacity(float(opacity))
                prop.SetEdgeVisibility(bool(edge_vis))
                prop.SetLineWidth(float(line_width))
                prop.SetEdgeColor(*edge_color)
            except Exception as _exc:
                log_exception(__name__, "_clear_rack_isolation_visual", _exc)
        self._rack_isolation_visual_props = []

    def _apply_rack_isolation_visual(self: "ModelViewWidget", rack_id: str) -> None:
        rid = str(rack_id or "")
        if not rid:
            return
        entry = None
        for candidate in self._rack_entries:
            if str(candidate.get("rack_id", "")) == rid:
                entry = candidate
                break
        if entry is None:
            return
        self._clear_rack_isolation_visual()
        snapshot: list[tuple[object, int, float, bool, float, tuple[float, float, float]]] = []
        for idx, actor in enumerate(entry.get("actors", [])):
            if actor is None:
                continue
            try:
                visibility = int(actor.GetVisibility())
                prop = actor.GetProperty()
                if prop is None:
                    continue
                edge_color = tuple(prop.GetEdgeColor())
                snapshot.append(
                    (
                        actor,
                        visibility,
                        float(prop.GetOpacity()),
                        bool(prop.GetEdgeVisibility()),
                        float(prop.GetLineWidth()),
                        (float(edge_color[0]), float(edge_color[1]), float(edge_color[2])),
                    )
                )
                is_pillar_actor = idx == 0
                actor.SetVisibility(1 if is_pillar_actor else 0)
                if is_pillar_actor:
                    prop.SetOpacity(1.0)
                    # Изоляция стойки на уровне дерева не должна отрисовывать контур выделения.
                    prop.SetEdgeVisibility(False)
                    prop.SetLineWidth(1.0)
            except Exception as _exc:
                log_exception(__name__, "_apply_rack_isolation_visual", _exc)
        self._rack_isolation_visual_props = snapshot


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Выделение стеллажа и визуальная изоляция контекста.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackSelectionMixin: точки входа
#       Публичные методы сценария:
#       - RackSelectionMixin.get_selected_rack_entry(...)
#       - RackSelectionMixin.get_selected_rack_display_color(...)
#       - RackSelectionMixin.rebuild_zone_racks_for_updated_zone(...)
#       - RackSelectionMixin.select_rack_by_id(...)
#       - RackSelectionMixin.clear_selected_rack(...)
#
#    B. RackSelectionMixin: запуск и настройка:
#       RackSelectionMixin._set_selected_rack(...)
#         Назначение: устанавливает selected rack в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackSelectionMixin._apply_selected_rack_visual(...)
#         -> RackSelectionMixin._clear_selected_rack_visual(...)
#       RackSelectionMixin._set_selected_rack_contour(...)
#         Назначение: устанавливает selected rack contour в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackSelectionMixin._clear_selected_rack_contour(...)
#
#    C. RackSelectionMixin: основной сценарий:
#       RackSelectionMixin.rebuild_zone_racks_for_updated_zone(...)
#         Назначение: Пересоздать акторы стоек/полок для зоны после обновления высоты/объёма зоны.
#         Последовательность внутренних вызовов:
#         -> RackSelectionMixin._apply_selected_rack_visual(...)
#       RackSelectionMixin.select_rack_by_id(...)
#         Назначение: выполняет шаг "select rack by id" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackSelectionMixin._set_selected_rack(...)
#       RackSelectionMixin._apply_selected_rack_visual(...)
#         Назначение: применяет selected rack visual в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackSelectionMixin._clear_selected_rack_visual(...)
#         -> RackSelectionMixin._set_selected_rack_contour(...)
#       RackSelectionMixin._apply_rack_isolation_visual(...)
#         Назначение: применяет rack isolation visual в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackSelectionMixin._clear_rack_isolation_visual(...)
#
#    D. RackSelectionMixin: завершение и очистка:
#       RackSelectionMixin.clear_selected_rack(...)
#         Назначение: Сбросить визуальное состояние выделенной стойки и контур наведения.
#         Последовательность внутренних вызовов:
#         -> RackSelectionMixin._set_selected_rack(...)
#       RackSelectionMixin._clear_selected_rack_visual(...)
#         Назначение: очищает selected rack visual в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackSelectionMixin._clear_selected_rack_contour(...)
#         -> RackSelectionMixin._apply_rack_isolation_visual(...)
#       RackSelectionMixin._clear_selected_rack_contour(...)
#         Назначение: очищает selected rack contour в рамках текущего сценария модуля.
#       RackSelectionMixin._clear_rack_isolation_visual(...)
#         Назначение: очищает rack isolation visual в рамках текущего сценария модуля.
#
#    E. RackSelectionMixin: вспомогательные расчёты:
#       RackSelectionMixin.get_selected_rack_entry(...)
#         Назначение: возвращает selected rack entry в рамках текущего сценария модуля.
#       RackSelectionMixin.get_selected_rack_display_color(...)
#         Назначение: возвращает selected rack display color в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackSelectionMixin.get_selected_rack_entry(...)
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Геометрическая визуализация зависит от pyvista/vtk; при недоступности модуль обязан завершать шаг без падения сценария.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
