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

from __future__ import annotations

from typing import Any, TYPE_CHECKING

try:
    import pyvista as pv

    _PV = True
except ImportError:
    _PV = False

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

if TYPE_CHECKING:
    from gui.components.model_view_widget import ModelViewWidget


class RackHoverMixin:
    """Логика подсветки при наведении и активного выделения для стеллажей и слотов полок."""

    def _actor_contour_mesh(self: "ModelViewWidget", actor):
        if not _PV or actor is None:
            return None
        try:
            bounds = actor.GetBounds()
        except Exception as exc:
            log_exception(__name__, "_actor_contour_mesh", exc)
            return None
        if not bounds or len(bounds) != 6:
            return None
        min_x, max_x, min_y, max_y, min_z, max_z = [float(v) for v in bounds]
        points = [
            (min_x, min_y, min_z),
            (max_x, min_y, min_z),
            (max_x, max_y, min_z),
            (min_x, max_y, min_z),
            (min_x, min_y, max_z),
            (max_x, min_y, max_z),
            (max_x, max_y, max_z),
            (min_x, max_y, max_z),
        ]
        edges = (
            (0, 1), (1, 2), (2, 3), (3, 0),
            (4, 5), (5, 6), (6, 7), (7, 4),
            (0, 4), (1, 5), (2, 6), (3, 7),
        )
        lines = []
        for i, j in edges:
            lines.extend([2, i, j])
        contour = pv.PolyData(points)
        contour.lines = lines
        return contour

    def _highlight_selected_rendered_shelf(
        self: "ModelViewWidget",
        rack_id: str,
        slot_id: str,
        shelf_index: int,
    ) -> None:
        rid = str(rack_id or "")
        sid = str(slot_id or "")
        idx = max(1, int(shelf_index or 1))
        if not rid or not sid or not self._plotter:
            self._clear_selected_rendered_shelf(render=False)
            return
        if (
            str(self._selected_shelf_bbox_rack_id or "") == rid
            and str(self._selected_shelf_bbox_slot_id or "") == sid
            and int(self._selected_shelf_bbox_index or 0) == idx
            and self._selected_shelf_bbox_actor is not None
        ):
            return
        self._clear_selected_rendered_shelf(render=False)
        actors = list((self._rack_shelf_actors or {}).get((rid, sid)) or [])
        if not actors:
            return
        actor = actors[min(len(actors) - 1, max(0, idx - 1))]
        contour = self._actor_contour_mesh(actor)
        if contour is None:
            return
        try:
            self._selected_shelf_bbox_actor = self._plotter.add_mesh(
                contour,
                color=(1.0, 1.0, 0.3),
                line_width=3.0,
                pickable=False,
                name="_selected_shelf_bbox_contour",
            )
            self._selected_shelf_bbox_rack_id = rid
            self._selected_shelf_bbox_slot_id = sid
            self._selected_shelf_bbox_index = idx
        except Exception as exc:
            log_exception(__name__, "_highlight_selected_rendered_shelf", exc)
            self._selected_shelf_bbox_actor = None
            self._selected_shelf_bbox_rack_id = None
            self._selected_shelf_bbox_slot_id = None
            self._selected_shelf_bbox_index = None
            return
        if hasattr(self, "_safe_render"):
            self._safe_render(min_interval_s=1.0 / 60.0)
        else:
            self._plotter.update()

    def _floor_shelf_plane_mesh(self: "ModelViewWidget", slot_actor):
        if not _PV or slot_actor is None:
            return None
        try:
            bounds = slot_actor.GetBounds()
        except Exception as exc:
            log_exception(__name__, "_floor_shelf_plane_mesh", exc)
            return None
        if not bounds or len(bounds) != 6:
            return None
        min_x, max_x, min_y, max_y, min_z, _max_z = [float(v) for v in bounds]
        # Небольшой подъём полигона над базовой плоскостью, чтобы исключить z-fighting.
        plane_z = float(min_z + 2.0)
        points = [
            (min_x, min_y, plane_z),
            (max_x, min_y, plane_z),
            (max_x, max_y, plane_z),
            (min_x, max_y, plane_z),
        ]
        plane = pv.PolyData(points)
        plane.faces = [4, 0, 1, 2, 3]
        return plane

    def _highlight_floor_shelf_polygon(
        self: "ModelViewWidget",
        rack_id: str,
        slot_id: str,
    ) -> None:
        rid = str(rack_id or "")
        sid = str(slot_id or "")
        if not rid or not sid or not self._plotter:
            self._clear_floor_shelf_polygon(render=False)
            return
        if (
            str(self._selected_floor_shelf_rack_id or "") == rid
            and str(self._selected_floor_shelf_slot_id or "") == sid
            and self._selected_floor_shelf_actor is not None
        ):
            return
        bbox_actor = getattr(self, "_selected_shelf_bbox_actor", None)
        if bbox_actor is not None and self._plotter:
            try:
                self._plotter.remove_actor(bbox_actor)
            except Exception as _exc:
                log_exception(__name__, "_highlight_floor_shelf_polygon", _exc)
        self._selected_shelf_bbox_actor = None
        self._selected_shelf_bbox_rack_id = None
        self._selected_shelf_bbox_slot_id = None
        self._selected_shelf_bbox_index = None
        self._clear_floor_shelf_polygon(render=False)
        entry = self._get_rack_entry(rid, self._shelf_target_zone_id)
        if entry is None:
            entry = self._get_rack_entry(rid)
        if entry is None:
            return
        slot_actor = (entry.get("slot_actors") or {}).get(sid)
        mesh = self._floor_shelf_plane_mesh(slot_actor)
        if mesh is None:
            return
        try:
            self._selected_floor_shelf_actor = self._plotter.add_mesh(
                mesh,
                color=(1.0, 1.0, 0.3),
                edge_color=(1.0, 1.0, 0.3),
                show_edges=True,
                opacity=0.28,
                line_width=2.5,
                pickable=False,
                name="_selected_floor_shelf_polygon",
            )
            self._selected_floor_shelf_rack_id = rid
            self._selected_floor_shelf_slot_id = sid
        except Exception as exc:
            log_exception(__name__, "_highlight_floor_shelf_polygon", exc)
            self._selected_floor_shelf_actor = None
            self._selected_floor_shelf_rack_id = None
            self._selected_floor_shelf_slot_id = None
            return
        if hasattr(self, "_safe_render"):
            self._safe_render(min_interval_s=1.0 / 60.0)
        else:
            self._plotter.update()

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

    def _clear_selected_rendered_shelf(self: "ModelViewWidget", render: bool = True) -> None:
        self._clear_floor_shelf_polygon(render=False)
        actor = getattr(self, "_selected_shelf_bbox_actor", None)
        if actor is not None and self._plotter:
            try:
                self._plotter.remove_actor(actor)
            except Exception as _exc:
                log_exception(__name__, "_clear_selected_rendered_shelf", _exc)
        self._selected_shelf_bbox_actor = None
        self._selected_shelf_bbox_rack_id = None
        self._selected_shelf_bbox_slot_id = None
        self._selected_shelf_bbox_index = None
        if not render:
            return
        if hasattr(self, "_safe_render"):
            self._safe_render(min_interval_s=1.0 / 60.0)
        elif self._plotter:
            self._plotter.update()

    def _highlight_shelf_slot_hover(self: "ModelViewWidget", slot_id: str) -> None:
        rid = str(self._shelf_target_rack_id or "")
        zid = str(self._shelf_target_zone_id or "")
        entry = self._get_rack_entry(rid, zid)
        if entry is None or not self._plotter:
            return
        self._clear_shelf_slot_hover()
        slot_actor = (entry.get("slot_actors") or {}).get(str(slot_id))
        contour = self._slot_contour_mesh(slot_actor)
        if contour is None:
            return
        try:
            self._hover_shelf_slot_actor = self._plotter.add_mesh(
                contour,
                color=(1.0, 1.0, 0.3),
                line_width=3.0,
                pickable=False,
                name="_hover_shelf_slot_contour",
            )
            self._hover_shelf_slot_id = str(slot_id)
        except Exception as exc:
            log_exception(__name__, "_highlight_shelf_slot_hover", exc)
            self._hover_shelf_slot_actor = None
            self._hover_shelf_slot_id = None
            return
        self._plotter.update()

    def _clear_shelf_slot_hover(self: "ModelViewWidget") -> None:
        actor = getattr(self, "_hover_shelf_slot_actor", None)
        if actor is not None and self._plotter:
            try:
                self._plotter.remove_actor(actor)
            except Exception as _exc:
                log_exception(__name__, "_clear_shelf_slot_hover", _exc)
        self._hover_shelf_slot_actor = None
        self._hover_shelf_slot_id = None

    def _set_active_shelf_slot(self: "ModelViewWidget", slot_id: str) -> None:
        rid = str(self._shelf_target_rack_id or "")
        zid = str(self._shelf_target_zone_id or "")
        entry = self._get_rack_entry(rid, zid)
        if entry is None or not self._plotter:
            return
        self._clear_selected_rendered_shelf(render=False)
        self._clear_shelf_slot_active()
        self._clear_shelf_slot_hover()
        slot_actor = (entry.get("slot_actors") or {}).get(str(slot_id))
        contour = self._slot_contour_mesh(slot_actor)
        if contour is None:
            return
        try:
            self._active_shelf_slot_actor = self._plotter.add_mesh(
                contour,
                color=(0.25, 0.55, 1.0),
                line_width=3.0,
                pickable=False,
                name="_active_shelf_slot_contour",
            )
            self._active_shelf_slot_id = str(slot_id)
        except Exception as exc:
            log_exception(__name__, "_set_active_shelf_slot", exc)
            self._active_shelf_slot_actor = None
            self._active_shelf_slot_id = None
        self._plotter.update()

    def _clear_shelf_slot_active(self: "ModelViewWidget") -> None:
        actor = getattr(self, "_active_shelf_slot_actor", None)
        if actor is not None and self._plotter:
            try:
                self._plotter.remove_actor(actor)
            except Exception as _exc:
                log_exception(__name__, "_clear_shelf_slot_active", _exc)
        self._active_shelf_slot_actor = None
        self._active_shelf_slot_id = None

    def _find_rack_id_by_actor(self: "ModelViewWidget", picked_actor, zone_id: str) -> str | None:
        zid = str(zone_id or "")
        if picked_actor is None:
            return None
        for entry in reversed(self._rack_entries):
            if zid and str(entry.get("zone_id", "")) != zid:
                continue
            for actor in entry.get("actors", []):
                if actor is None:
                    continue
                try:
                    if not bool(actor.GetVisibility()):
                        continue
                except Exception as _exc:
                    log_exception(__name__, "_find_rack_id_by_actor", _exc)
                if actor is picked_actor or actor == picked_actor:
                    return str(entry.get("rack_id", ""))
                try:
                    if (
                        hasattr(actor, "GetAddressAsString")
                        and hasattr(picked_actor, "GetAddressAsString")
                        and actor.GetAddressAsString("") == picked_actor.GetAddressAsString("")
                    ):
                        return str(entry.get("rack_id", ""))
                except Exception as _exc:
                    log_exception(__name__, "_find_rack_id_by_actor", _exc)
        return None

    def _find_rack_id_by_point(self: "ModelViewWidget", x: float, y: float, zone_id: str) -> str | None:
        zid = str(zone_id or "")
        for entry in reversed(self._rack_entries):
            if zid and str(entry.get("zone_id", "")) != zid:
                continue
            # Пропускаем скрытые импортированные модели, чтобы они не перехватывали выбор.
            if not self._imported_models_enabled and self._is_imported_model_entry(entry):
                continue
            min_x, max_x, min_y, max_y = self._rack_pick_bbox(entry)
            if min_x <= x <= max_x and min_y <= y <= max_y:
                return str(entry.get("rack_id", ""))
        return None

    def _rack_pick_bbox(self: "ModelViewWidget", entry: dict[str, Any]) -> tuple[float, float, float, float]:
        """Полный bbox габаритов для выбора (включая стойки/рёбра)."""
        center = entry.get("center") or (0.0, 0.0)
        params = dict(entry.get("params") or {})
        rotation = int(entry.get("rotation", 0)) % 360
        return entry.get("bbox") or rack_bbox(
            float(center[0]), float(center[1]), params, rotation,
        )

    def _rack_container_bbox(self: "ModelViewWidget", entry: dict[str, Any]) -> tuple[float, float, float, float]:
        """Внешние XY-границы геометрии стеллажа (без вспомогательных проходов/буферов)."""
        actors = list(entry.get("actors") or [])
        primary_actor = actors[0] if actors else None
        if primary_actor is not None:
            try:
                b = primary_actor.GetBounds()
                if b and len(b) == 6:
                    min_x, max_x, min_y, max_y = float(b[0]), float(b[1]), float(b[2]), float(b[3])
                    if min_x < max_x and min_y < max_y:
                        return (min_x, max_x, min_y, max_y)
            except Exception as _exc:
                log_exception(__name__, "_rack_container_bbox", _exc)
        # Резервный вариант: параметрическая аппроксимация при недоступных границах актёра.
        center = entry.get("center") or (0.0, 0.0)
        params = dict(entry.get("params") or {})
        rotation = int(entry.get("rotation", 0)) % 360
        if self._has_additional_model(params):
            return entry.get("bbox") or rack_bbox(float(center[0]), float(center[1]), params, rotation)
        cx = float(center[0])
        cy = float(center[1])

        if self._is_pallet_params(params):
            width = max(10.0, float(params.get("footprint_width_mm", self._PALLET_FOOTPRINT_WIDTH_SINGLE_MM)))
            depth = max(10.0, float(params.get("footprint_depth_mm", self._PALLET_DEPTH_MM)))
        else:
            width = max(10.0, float(params.get("footprint_width_mm", 1000)) - 60.0)
            depth = max(10.0, float(params.get("footprint_depth_mm", 500)) - 60.0)
        if int(rotation) % 180 == 90:
            width, depth = depth, width
        return (
            cx - width / 2.0,
            cx + width / 2.0,
            cy - depth / 2.0,
            cy + depth / 2.0,
        )

    def _build_rack_hover_contour_mesh(self: "ModelViewWidget", entry: dict[str, Any]):
        if not _PV:
            return None
        bbox = self._rack_container_bbox(entry)
        min_x, max_x, min_y, max_y = bbox
        bottom_z = None
        top_z = None
        actors = list(entry.get("actors") or [])
        primary_actor = actors[0] if actors else None
        if primary_actor is not None:
            try:
                b = primary_actor.GetBounds()
                if b and len(b) == 6:
                    min_z, max_z = float(b[4]), float(b[5])
                    if min_z < max_z:
                        bottom_z = min_z + 2.0
                        top_z = max_z + 2.0
            except Exception as _exc:
                log_exception(__name__, "_build_rack_hover_contour_mesh", _exc)
        if bottom_z is None or top_z is None:
            params = dict(entry.get("params") or {})
            z_ref = float(self._zone_heights.get(str(entry.get("zone_id") or ""), (0.0, 0.0))[0])
            bottom_z = float(z_ref + 2.0)
            top_z = float(z_ref + max(10.0, self._rack_height_mm(params)) + 2.0)
        points = [
            # Нижний прямоугольник.
            (float(min_x), float(min_y), bottom_z),  # 0
            (float(max_x), float(min_y), bottom_z),  # 1
            (float(max_x), float(max_y), bottom_z),  # 2
            (float(min_x), float(max_y), bottom_z),  # 3
            # Верхний прямоугольник.
            (float(min_x), float(min_y), top_z),     # 4
            (float(max_x), float(min_y), top_z),     # 5
            (float(max_x), float(max_y), top_z),     # 6
            (float(min_x), float(max_y), top_z),     # 7
        ]
        line_cells = []
        edges = (
            (0, 1), (1, 2), (2, 3), (3, 0),  # низ
            (4, 5), (5, 6), (6, 7), (7, 4),  # верх
            (0, 4), (1, 5), (2, 6), (3, 7),  # вертикали
        )
        for i, j in edges:
            line_cells.extend([2, i, j])
        contour = pv.PolyData(points)
        contour.lines = line_cells
        return contour

    def _highlight_hover_rack(self: "ModelViewWidget", rack_id: str) -> None:
        rid = str(rack_id or "")
        if not rid:
            self._clear_hover_rack_highlight()
            return
        if rid == str(self._hover_rack_id or "") and self._hover_rack_contour_actor is not None:
            return
        self._clear_hover_rack_highlight(render=False)
        entry = None
        for candidate in self._rack_entries:
            if str(candidate.get("rack_id", "")) == rid:
                entry = candidate
                break
        if entry is None or not self._plotter:
            return
        contour = self._build_rack_hover_contour_mesh(entry)
        if contour is None:
            return
        try:
            self._hover_rack_contour_actor = self._plotter.add_mesh(
                contour,
                color=(1.0, 1.0, 0.3),
                line_width=3.0,
                name="_hover_rack_contour",
                pickable=False,
            )
            self._hover_rack_id = rid
        except Exception as exc:
            log_exception(__name__, "_highlight_hover_rack", exc)
            self._hover_rack_contour_actor = None
            self._hover_rack_id = None
            return
        if hasattr(self, "_safe_render"):
            self._safe_render(min_interval_s=1.0 / 60.0)
        elif self._plotter:
            self._plotter.update()

    def _clear_hover_rack_highlight(self: "ModelViewWidget", render: bool = True) -> None:
        actor = getattr(self, "_hover_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_hover_rack_highlight", _exc)
        self._hover_rack_contour_actor = None
        self._hover_rack_id = None
        if render:
            if hasattr(self, "_safe_render"):
                self._safe_render(min_interval_s=1.0 / 60.0)
            elif self._plotter:
                self._plotter.update()


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Наведение и подсветка стеллажей и ячеек.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackHoverMixin: точки входа
#       Публичные методы отсутствуют; сценарий запускается через методы родительских модулей и внутренние обработчики.
#
#    B. RackHoverMixin: запуск и настройка:
#       RackHoverMixin._set_active_shelf_slot(...)
#         Назначение: устанавливает active shelf slot в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackHoverMixin._clear_shelf_slot_active(...)
#         -> RackHoverMixin._clear_shelf_slot_hover(...)
#
#    C. RackHoverMixin: основной сценарий:
#       RackHoverMixin._find_rack_id_by_actor(...)
#         Назначение: находит rack id by actor в рамках текущего сценария модуля.
#       RackHoverMixin._find_rack_id_by_point(...)
#         Назначение: находит rack id by point в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackHoverMixin._rack_pick_bbox(...)
#
#    D. RackHoverMixin: завершение и очистка:
#       RackHoverMixin._clear_shelf_slot_hover(...)
#         Назначение: очищает shelf slot hover в рамках текущего сценария модуля.
#       RackHoverMixin._clear_shelf_slot_active(...)
#         Назначение: очищает shelf slot active в рамках текущего сценария модуля.
#       RackHoverMixin._clear_hover_rack_highlight(...)
#         Назначение: очищает hover rack highlight в рамках текущего сценария модуля.
#
#    E. RackHoverMixin: вспомогательные расчёты:
#       RackHoverMixin._highlight_shelf_slot_hover(...)
#         Назначение: подсвечивает shelf slot hover в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackHoverMixin._clear_shelf_slot_hover(...)
#       RackHoverMixin._rack_pick_bbox(...)
#         Назначение: Полный bbox габаритов для выбора (включая стойки/рёбра).
#       RackHoverMixin._rack_container_bbox(...)
#         Назначение: Внешние XY-границы геометрии стеллажа (без вспомогательных проходов/буферов).
#       RackHoverMixin._build_rack_hover_contour_mesh(...)
#         Назначение: строит rack hover contour mesh в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackHoverMixin._rack_container_bbox(...)
#       RackHoverMixin._highlight_hover_rack(...)
#         Назначение: подсвечивает hover rack в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackHoverMixin._clear_hover_rack_highlight(...)
#         -> RackHoverMixin._build_rack_hover_contour_mesh(...)
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Геометрическая визуализация зависит от pyvista/vtk; при недоступности модуль обязан завершать шаг без падения сценария.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
