# -*- coding: utf-8 -*-
"""Логика выбора стоек и ячеек полок (экран → мировое лучевое пересечение,
наведение, клик), извлечённая из монолитного миксина стоек."""

from __future__ import annotations

from pathlib import Path
from typing import Any, TYPE_CHECKING

from gui.components.model_view._mv_racks_lifecycle import _ray_aabb_intersect
from gui.components.model_view._mv_rack_geometry import rotate_xy
from error_logger import log_exception

if TYPE_CHECKING:
    from gui.components.model_view_widget import ModelViewWidget


class RackPickingMixin:
    """Вспомогательные функции выбора – проверка попадания в стойку / ячейку полки по экранным координатам."""

    def _on_select_rack_click(self: "ModelViewWidget", x: float, y: float, z: float) -> bool:
        if not self._interaction_manager.is_active("rack_select"):
            return False
        zone_id = str(self._rack_preview_zone_id or "")
        locked_rack_id = str(self._rack_isolation_rack_id or "") if bool(self._rack_isolation_active) else ""
        # Предпочитать текущую стойку под курсором для детерминированности ЛКМ.
        rack_id = str(self._hover_rack_id or "")
        if not rack_id and self._plotter and self._plotter.interactor:
            try:
                sx, sy = self._plotter.interactor.GetEventPosition()
                rack_id = self._find_rack_id_by_screen(float(sx), float(sy), zone_id, screen_is_vtk=True)
            except Exception as exc:
                log_exception(__name__, "_on_select_rack_click", exc)
                rack_id = None
        if (not rack_id) and x is not None and y is not None:
            rack_id = self._find_rack_id_by_point(float(x), float(y), zone_id)
        if locked_rack_id and rack_id and str(rack_id) != locked_rack_id:
            return True
        if not rack_id:
            self._set_selected_rack(None)
            self._clear_hover_rack_highlight()
            return True
        self._set_selected_rack(rack_id)
        return True

    def _rack_anchor_world_point(
        self: "ModelViewWidget",
        center_x: float,
        center_y: float,
        params: dict[str, Any],
        rotation: int,
    ) -> tuple[float, float]:
        width = float(params.get("footprint_width_mm", 1000))
        depth = float(params.get("footprint_depth_mm", 500))
        numbering_direction = str(params.get("numbering_direction") or "left_to_right")
        start_from_right = numbering_direction == "right_to_left"
        local_x = (width / 2.0) if start_from_right else (-width / 2.0)
        local_y = -depth / 2.0  # задняя сторона (передняя — +Y в локальных координатах стойки)
        dx, dy = rotate_xy(local_x, local_y, rotation)
        return (float(center_x) + float(dx), float(center_y) + float(dy))

    def _center_from_anchor_world_point(
        self: "ModelViewWidget",
        anchor_x: float,
        anchor_y: float,
        params: dict[str, Any],
        rotation: int,
    ) -> tuple[float, float]:
        width = float(params.get("footprint_width_mm", 1000))
        depth = float(params.get("footprint_depth_mm", 500))
        numbering_direction = str(params.get("numbering_direction") or "left_to_right")
        start_from_right = numbering_direction == "right_to_left"
        local_x = (width / 2.0) if start_from_right else (-width / 2.0)
        local_y = -depth / 2.0
        dx, dy = rotate_xy(local_x, local_y, rotation)
        return (float(anchor_x) - float(dx), float(anchor_y) - float(dy))

    def _on_select_rack_hover_screen(self: "ModelViewWidget", sx: float, sy: float) -> None:
        if not self._interaction_manager.is_active("rack_select"):
            self._clear_hover_rack_highlight()
            return
        zone_id = str(self._rack_preview_zone_id or "")
        locked_rack_id = str(self._rack_isolation_rack_id or "") if bool(self._rack_isolation_active) else ""
        rack_id = self._find_rack_id_by_screen(float(sx), float(sy), zone_id, screen_is_vtk=False)
        if locked_rack_id and rack_id and str(rack_id) != locked_rack_id:
            self._clear_hover_rack_highlight()
            return
        if not rack_id:
            self._clear_hover_rack_highlight()
            return
        self._highlight_hover_rack(rack_id)

    def _find_rack_id_by_screen(
        self: "ModelViewWidget",
        sx: float,
        sy: float,
        zone_id: str,
        screen_is_vtk: bool = False,
    ) -> str | None:
        if not self._plotter or not self._plotter.renderer:
            return None

        # 1) Построить луч камеры (p0 → p1) из экранного пикселя.
        ray = self._screen_to_camera_ray(sx, sy, screen_is_vtk=screen_is_vtk)
        if ray is not None:
            result = self._find_rack_id_by_ray(ray[0], ray[1], zone_id)
            if result:
                return result

        # 2) Запасной вариант — выбор актора (CellPicker с допуском по рёбрам).
        try:
            from vtkmodules.vtkRenderingCore import vtkCellPicker
            pick_x = int(round(float(sx)))
            pick_y = int(round(float(sy)))
            if not screen_is_vtk:
                dpr = self._plotter.devicePixelRatio()
                pick_x = int(round(float(sx) * dpr))
                pick_y = int(round(float(sy) * dpr))
                rw = self._plotter.ren_win
                if rw:
                    _, h = rw.GetSize()
                    pick_y = h - pick_y - 1
            cell_picker = vtkCellPicker()
            cell_picker.SetTolerance(0.005)
            cell_picker.Pick(pick_x, pick_y, 0, self._plotter.renderer)
            picked_actor = cell_picker.GetViewProp()
            if picked_actor is not None:
                return self._find_rack_id_by_actor(picked_actor, zone_id)
        except Exception as exc:
            log_exception(__name__, "_find_rack_id_by_screen", exc)
            return None
        return None

    def _screen_to_camera_ray(
        self: "ModelViewWidget",
        sx: float,
        sy: float,
        screen_is_vtk: bool = False,
    ) -> tuple[tuple[float, float, float], tuple[float, float, float]] | None:
        """Вернуть мировые координаты (near_point, far_point) для экранного пикселя."""
        if not self._plotter or not self._plotter.renderer:
            return None
        px = float(sx)
        py = float(sy)
        if not screen_is_vtk and getattr(self._plotter, "ren_win", None):
            try:
                dpr = float(self._plotter.devicePixelRatio())
                if dpr <= 0.0:
                    dpr = 1.0
                px *= dpr
                py *= dpr
                _vw, vh = self._plotter.ren_win.GetSize()
                py = float(vh) - py - 1.0
            except Exception as exc:
                log_exception(__name__, "_screen_to_camera_ray.scale", exc)
                px, py = float(sx), float(sy)
        try:
            renderer = self._plotter.renderer
            renderer.SetDisplayPoint(px, py, 0)
            renderer.DisplayToWorld()
            wp0 = renderer.GetWorldPoint()
            renderer.SetDisplayPoint(px, py, 1)
            renderer.DisplayToWorld()
            wp1 = renderer.GetWorldPoint()
            if wp0[3] == 0 or wp1[3] == 0:
                return None
            p0 = (wp0[0] / wp0[3], wp0[1] / wp0[3], wp0[2] / wp0[3])
            p1 = (wp1[0] / wp1[3], wp1[1] / wp1[3], wp1[2] / wp1[3])
            return (p0, p1)
        except Exception as exc:
            log_exception(__name__, "_screen_to_camera_ray", exc)
            return None

    def _find_rack_id_by_ray(
        self: "ModelViewWidget",
        p0: tuple[float, float, float],
        p1: tuple[float, float, float],
        zone_id: str,
    ) -> str | None:
        """Проверить луч камеры по 3D-ограничивающим параллелепипедам всех стоек в зоне."""
        zid = str(zone_id or "")
        dx = p1[0] - p0[0]
        dy = p1[1] - p0[1]
        dz = p1[2] - p0[2]
        best_t = float("inf")
        best_id: str | None = None
        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)
            params = dict(entry.get("params") or {})
            z_ref = float(self._zone_heights.get(str(entry.get("zone_id") or ""), (0.0, 0.0))[0])
            min_z = z_ref
            max_z = z_ref + max(10.0, self._rack_height_mm(params))
            t = _ray_aabb_intersect(p0, dx, dy, dz, min_x, max_x, min_y, max_y, min_z, max_z)
            if t is not None and t < best_t:
                best_t = t
                best_id = str(entry.get("rack_id", ""))
        return best_id

    def _on_select_shelf_slot_hover_screen(self: "ModelViewWidget", sx: float, sy: float) -> None:
        if not self._interaction_manager.is_active("shelf_placement"):
            self._clear_shelf_slot_hover()
            return
        rack_id = str(self._shelf_target_rack_id or "")
        zone_id = str(self._shelf_target_zone_id or "")
        slot_id = self._find_slot_id_by_screen(float(sx), float(sy), rack_id, zone_id, screen_is_vtk=False)
        if not slot_id:
            # Сохранить текущий контур наведения, чтобы избежать визуального мерцания при промахах.
            return
        if slot_id == str(self._active_shelf_slot_id or ""):
            return
        if slot_id == str(self._hover_shelf_slot_id or ""):
            return
        self._highlight_shelf_slot_hover(slot_id)

    def _on_select_shelf_slot_click(self: "ModelViewWidget", x: float, y: float, z: float) -> bool:
        if not self._interaction_manager.is_active("shelf_placement"):
            return False
        rack_id = str(self._shelf_target_rack_id or "")
        zone_id = str(self._shelf_target_zone_id or "")
        slot_id = str(self._hover_shelf_slot_id or "")
        if (not slot_id) and self._plotter and self._plotter.interactor:
            try:
                sx, sy = self._plotter.interactor.GetEventPosition()
                slot_id = str(
                    self._find_slot_id_by_screen(float(sx), float(sy), rack_id, zone_id, screen_is_vtk=True) or ""
                )
            except Exception as exc:
                log_exception(__name__, "_on_select_shelf_slot_click", exc)
                slot_id = ""
        if not slot_id:
            return True
        self._set_active_shelf_slot(slot_id)
        self._selected_shelf_visual_rack_id = rack_id
        self._selected_shelf_visual_slot_id = slot_id
        self._selected_shelf_visual_index = 1
        try:
            self.shelf_slot_selected.emit(rack_id, slot_id)  # type: ignore[attr-defined]
        except Exception as _exc:
            log_exception(__name__, "_on_select_shelf_slot_click", _exc)
        return True

    def _find_rendered_shelf_by_screen(
        self: "ModelViewWidget",
        sx: float,
        sy: float,
        rack_id: str,
        *,
        screen_is_vtk: bool = False,
    ) -> tuple[str, int] | None:
        rid = str(rack_id or "")
        if not rid or not self._plotter or not self._plotter.renderer:
            return None

        try:
            from vtkmodules.vtkRenderingCore import vtkPropPicker

            pick_x = int(round(float(sx)))
            pick_y = int(round(float(sy)))
            if not screen_is_vtk:
                dpr = self._plotter.devicePixelRatio()
                pick_x = int(round(float(sx) * dpr))
                pick_y = int(round(float(sy) * dpr))
                rw = self._plotter.ren_win
                if rw:
                    _, h = rw.GetSize()
                    pick_y = h - pick_y - 1
            picker = vtkPropPicker()
            picker.Pick(pick_x, pick_y, 0, self._plotter.renderer)
            picked_actor = picker.GetViewProp()
            if picked_actor is not None:
                for (entry_rid, slot_id), actors in (self._rack_shelf_actors or {}).items():
                    if str(entry_rid) != rid:
                        continue
                    for actor_index, actor in enumerate(list(actors or []), start=1):
                        if actor is None:
                            continue
                        try:
                            if not bool(actor.GetVisibility()):
                                continue
                        except Exception as exc:
                            log_exception(__name__, "_find_rendered_shelf_by_screen.visibility", exc)
                            continue
                        if actor is picked_actor or actor == picked_actor:
                            return (str(slot_id), int(actor_index))
                        try:
                            if (
                                hasattr(actor, "GetAddressAsString")
                                and hasattr(picked_actor, "GetAddressAsString")
                                and actor.GetAddressAsString("") == picked_actor.GetAddressAsString("")
                            ):
                                return (str(slot_id), int(actor_index))
                        except Exception as _exc:
                            log_exception(__name__, "_find_rendered_shelf_by_screen", _exc)
        except Exception as _exc:
            log_exception(__name__, "_find_rendered_shelf_by_screen", _exc)
        ray = self._screen_to_camera_ray(sx, sy, screen_is_vtk=screen_is_vtk)
        if ray is None:
            return None
        p0, p1 = ray
        dx = float(p1[0] - p0[0])
        dy = float(p1[1] - p0[1])
        dz = float(p1[2] - p0[2])
        best_t = float("inf")
        best_hit: tuple[str, int] | None = None
        for (entry_rid, slot_id), actors in (self._rack_shelf_actors or {}).items():
            if str(entry_rid) != rid:
                continue
            for actor_index, actor in enumerate(list(actors or []), start=1):
                if actor is None:
                    continue
                try:
                    if not bool(actor.GetVisibility()):
                        continue
                    bounds = actor.GetBounds()
                except Exception as exc:
                    log_exception(__name__, "_find_rendered_shelf_by_screen.bounds", exc)
                    continue
                if not bounds or len(bounds) != 6:
                    continue
                min_x, max_x, min_y, max_y, min_z, max_z = [float(v) for v in bounds]
                t = _ray_aabb_intersect(p0, dx, dy, dz, min_x, max_x, min_y, max_y, min_z, max_z)
                if t is not None and t < best_t:
                    best_t = float(t)
                    best_hit = (str(slot_id), int(actor_index))
        return best_hit

    def _find_slot_id_by_screen(
        self: "ModelViewWidget",
        sx: float,
        sy: float,
        rack_id: str,
        zone_id: str,
        screen_is_vtk: bool = False,
        include_hidden: bool = False,
    ) -> str | None:
        entry = self._get_rack_entry(rack_id, zone_id)
        if entry is None or not self._plotter or not self._plotter.renderer:
            return None
        try:
            from vtkmodules.vtkRenderingCore import vtkPropPicker

            pick_x = int(round(float(sx)))
            pick_y = int(round(float(sy)))
            if not screen_is_vtk:
                dpr = self._plotter.devicePixelRatio()
                pick_x = int(round(float(sx) * dpr))
                pick_y = int(round(float(sy) * dpr))
                rw = self._plotter.ren_win
                if rw:
                    _, h = rw.GetSize()
                    pick_y = h - pick_y - 1
            picker = vtkPropPicker()
            picker.Pick(pick_x, pick_y, 0, self._plotter.renderer)
            picked_actor = picker.GetViewProp()
            if picked_actor is not None:
                for slot_id, slot_actor in (entry.get("slot_actors") or {}).items():
                    if slot_actor is None:
                        continue
                    try:
                        if (not include_hidden) and (not bool(slot_actor.GetVisibility())):
                            continue
                    except Exception as _exc:
                        log_exception(__name__, "_find_slot_id_by_screen", _exc)
                    if slot_actor is picked_actor or slot_actor == picked_actor:
                        return str(slot_id)
                    try:
                        if (
                            hasattr(slot_actor, "GetAddressAsString")
                            and hasattr(picked_actor, "GetAddressAsString")
                            and slot_actor.GetAddressAsString("") == picked_actor.GetAddressAsString("")
                        ):
                            return str(slot_id)
                    except Exception as _exc:
                        log_exception(__name__, "_find_slot_id_by_screen", _exc)
        except Exception as _exc:
            log_exception(__name__, "_find_slot_id_by_screen", _exc)
        # Запасной вариант для полностью прозрачных акторов ячеек:
        # определить ближайшую ячейку пересечением луча с ограничивающим параллелепипедом.
        ray = self._screen_to_camera_ray(sx, sy, screen_is_vtk=screen_is_vtk)
        if ray is None:
            return None
        p0, p1 = ray
        dx = float(p1[0] - p0[0])
        dy = float(p1[1] - p0[1])
        dz = float(p1[2] - p0[2])
        best_t = float("inf")
        best_slot_id: str | None = None
        for slot_id, slot_actor in (entry.get("slot_actors") or {}).items():
            if slot_actor is None:
                continue
            try:
                if (not include_hidden) and (not bool(slot_actor.GetVisibility())):
                    continue
                bounds = slot_actor.GetBounds()
            except Exception as exc:
                log_exception(__name__, "_find_slot_id_by_screen.bounds", exc)
                continue
            if not bounds or len(bounds) != 6:
                continue
            min_x, max_x, min_y, max_y, min_z, max_z = [float(v) for v in bounds]
            t = _ray_aabb_intersect(
                p0,
                dx,
                dy,
                dz,
                min_x,
                max_x,
                min_y,
                max_y,
                min_z,
                max_z,
            )
            if t is not None and t < best_t:
                best_t = float(t)
                best_slot_id = str(slot_id)
        return best_slot_id

    def _get_rack_entry(self: "ModelViewWidget", rack_id: str, zone_id: str | None = None) -> dict[str, Any] | None:
        rid = str(rack_id or "")
        zid = str(zone_id or "")
        if not rid:
            return None
        for entry in self._rack_entries:
            if str(entry.get("rack_id", "")) != rid:
                continue
            if zid and str(entry.get("zone_id", "")) != zid:
                continue
            return entry
        return None

    def _preload_shelf_model_for_entry(self: "ModelViewWidget", entry: dict[str, Any]) -> None:
        params = dict(entry.get("params") or {})
        rack_type = str(params.get("rack_type") or "A")
        depth = int(params.get("depth_mm") or params.get("footprint_depth_mm") or 500)
        widths = [int(v) for v in (params.get("span_widths_mm") or []) if int(v) > 0]
        if not widths:
            widths = [int(params.get("footprint_width_mm") or 1000)]
        self._get_shelf_base_mesh(rack_type, depth, widths[0])


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Выбор стеллажей и ячеек по экранным координатам и лучу камеры.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackPickingMixin: точки входа
#       Публичные методы отсутствуют; сценарий запускается через методы родительских модулей и внутренние обработчики.
#
#    B. RackPickingMixin: основной сценарий:
#       RackPickingMixin._on_select_rack_click(...)
#         Назначение: выполняет шаг "on select rack click" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPickingMixin._find_rack_id_by_screen(...)
#       RackPickingMixin._on_select_rack_hover_screen(...)
#         Назначение: выполняет шаг "on select rack hover screen" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPickingMixin._find_rack_id_by_screen(...)
#       RackPickingMixin._find_rack_id_by_screen(...)
#         Назначение: находит rack id by screen в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPickingMixin._screen_to_camera_ray(...)
#         -> RackPickingMixin._find_rack_id_by_ray(...)
#       RackPickingMixin._find_rack_id_by_ray(...)
#         Назначение: Проверить луч камеры по 3D-ограничивающим параллелепипедам всех стоек в зоне.
#       RackPickingMixin._on_select_shelf_slot_hover_screen(...)
#         Назначение: выполняет шаг "on select shelf slot hover screen" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPickingMixin._find_slot_id_by_screen(...)
#       RackPickingMixin._on_select_shelf_slot_click(...)
#         Назначение: выполняет шаг "on select shelf slot click" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPickingMixin._find_slot_id_by_screen(...)
#       RackPickingMixin._find_slot_id_by_screen(...)
#         Назначение: находит slot id by screen в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPickingMixin._get_rack_entry(...)
#         -> RackPickingMixin._screen_to_camera_ray(...)
#
#    C. RackPickingMixin: вспомогательные расчёты:
#       RackPickingMixin._rack_anchor_world_point(...)
#         Назначение: выполняет шаг "rack anchor world point" в рамках текущего сценария модуля.
#       RackPickingMixin._center_from_anchor_world_point(...)
#         Назначение: выполняет шаг "center from anchor world point" в рамках текущего сценария модуля.
#       RackPickingMixin._screen_to_camera_ray(...)
#         Назначение: Вернуть мировые координаты (near_point, far_point) для экранного пикселя.
#       RackPickingMixin._get_rack_entry(...)
#         Назначение: возвращает rack entry в рамках текущего сценария модуля.
#       RackPickingMixin._preload_shelf_model_for_entry(...)
#         Назначение: выполняет шаг "preload shelf model for entry" в рамках текущего сценария модуля.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
