# -*- coding: utf-8 -*-
"""Миксин перемещения/переноса стоек для ModelViewWidget."""

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

if TYPE_CHECKING:
    from gui.components.model_view_widget import ModelViewWidget


class RackMoveMixin:

    def begin_move_selected_rack(self: "ModelViewWidget") -> bool:
        entry = self.get_selected_rack_entry()
        if not entry:
            return False
        self._clear_selected_rack_visual()
        self._last_moved_rack_id = None
        zone_id = str(entry.get("zone_id", ""))
        params = dict(entry.get("params") or {})
        # Удалить старые акторы со сцены и переключиться в режим предпросмотра перемещения.
        self._moving_rack_entry = dict(entry)
        self._set_moving_rack_origin_ghost(self._moving_rack_entry, True)
        self._rack_entries = [e for e in self._rack_entries if str(e.get("rack_id", "")) != str(entry.get("rack_id", ""))]
        self.start_rack_placement(zone_id, params, self._rack_models_root, move_existing=True)
        self._set_selected_rack(str(entry.get("rack_id", "")))
        return True

    def _set_moving_rack_origin_ghost(self: "ModelViewWidget", entry: dict[str, Any], enabled: bool) -> None:
        rid = str(entry.get("rack_id") or "")
        if not rid:
            return
        if enabled:
            rack_snapshot: list[tuple[Any, int, float, bool, float, tuple[float, float, float]]] = []
            for actor in list(entry.get("actors", []) or []):
                if actor is None:
                    continue
                try:
                    prop = actor.GetProperty()
                    if prop is None:
                        continue
                    edge_color = tuple(prop.GetEdgeColor())
                    rack_snapshot.append(
                        (
                            actor,
                            int(actor.GetVisibility()),
                            float(prop.GetOpacity()),
                            bool(prop.GetEdgeVisibility()),
                            float(prop.GetLineWidth()),
                            (float(edge_color[0]), float(edge_color[1]), float(edge_color[2])),
                        )
                    )
                    actor.SetVisibility(0)
                except Exception as _exc:
                    log_exception(__name__, "_set_moving_rack_origin_ghost", _exc)
            entry["_origin_rack_visual_snapshot"] = rack_snapshot

            shelf_snapshot: list[tuple[Any, int, float]] = []
            for (entry_rid, _slot_id), actors in (self._rack_shelf_actors or {}).items():
                if str(entry_rid) != rid:
                    continue
                for actor in list(actors or []):
                    if actor is None:
                        continue
                    try:
                        prop = actor.GetProperty()
                        if prop is None:
                            continue
                        shelf_snapshot.append((actor, int(actor.GetVisibility()), float(prop.GetOpacity())))
                        actor.SetVisibility(0)
                    except Exception as _exc:
                        log_exception(__name__, "_set_moving_rack_origin_ghost", _exc)
            entry["_origin_shelf_visual_snapshot"] = shelf_snapshot
            return

        rack_snapshot = list(entry.get("_origin_rack_visual_snapshot") or [])
        for actor, visibility, opacity, edge_vis, line_width, edge_color in rack_snapshot:
            try:
                prop = actor.GetProperty()
                if prop is None:
                    continue
                actor.SetVisibility(int(visibility))
                prop.SetOpacity(float(opacity))
                prop.SetEdgeVisibility(bool(edge_vis))
                prop.SetLineWidth(float(line_width))
                prop.SetEdgeColor(float(edge_color[0]), float(edge_color[1]), float(edge_color[2]))
            except Exception as _exc:
                log_exception(__name__, "_set_moving_rack_origin_ghost", _exc)
        entry.pop("_origin_rack_visual_snapshot", None)

        shelf_snapshot = list(entry.get("_origin_shelf_visual_snapshot") or [])
        for actor, visibility, opacity in shelf_snapshot:
            try:
                prop = actor.GetProperty()
                if prop is None:
                    continue
                actor.SetVisibility(int(visibility))
                prop.SetOpacity(float(opacity))
            except Exception as _exc:
                log_exception(__name__, "_set_moving_rack_origin_ghost", _exc)
        entry.pop("_origin_shelf_visual_snapshot", None)

    def _restore_moving_rack_entry(self: "ModelViewWidget") -> bool:
        """Восстановить перемещённую стойку на исходную позицию при отмене."""
        entry = dict(self._moving_rack_entry or {})
        if not entry:
            self._moving_rack_entry = None
            return False
        self._set_moving_rack_origin_ghost(entry, False)
        if entry.get("actors"):
            center = entry.get("center") or (0.0, 0.0)
            params = dict(entry.get("params") or {})
            rotation = int(entry.get("rotation", 0)) % 360
            entry["bbox"] = rack_bbox(float(center[0]), float(center[1]), params, rotation)
            self._apply_rack_helper_visibility_policy(entry, True)
            self._rack_entries.append(entry)
            self._moving_rack_entry = None
            self._set_selected_rack(str(entry.get("rack_id", "")))
            if self._plotter:
                self._plotter.update()
            return True
        zone_id = str(entry.get("zone_id", ""))
        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]), zone_id, params, rotation, color, opacity,
        )
        if not actors:
            self._moving_rack_entry = None
            return False
        hidden_slot_ids = set(str(v) for v in (entry.get("hidden_slot_ids") or set()))
        for sid in hidden_slot_ids:
            actor = slot_actors.get(sid)
            if actor is not None:
                try:
                    actor.SetVisibility(0)
                except Exception as _exc:
                    log_exception(__name__, "_restore_moving_rack_entry", _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.append(entry)
        self._moving_rack_entry = None
        self._set_selected_rack(str(entry.get("rack_id", "")))
        return True

    def update_selected_rack_params(
        self: "ModelViewWidget",
        params: dict[str, Any],
        anchor_mode: str | None = None,
    ) -> bool:
        if not self.can_update_selected_rack_params(params, anchor_mode=anchor_mode):
            return False
        entry = self.get_selected_rack_entry()
        if not entry:
            return False
        zone_id = str(entry.get("zone_id", ""))
        center = entry.get("center") or (0.0, 0.0)
        rotation = int(entry.get("rotation", 0)) % 360
        rack_id = str(entry.get("rack_id", ""))
        anchor_world = None
        if str(anchor_mode or "").strip().lower() == "back_numbering_side":
            anchor_world = self._rack_anchor_world_point(
                float(center[0]),
                float(center[1]),
                dict(entry.get("params") or {}),
                rotation,
            )
        normalized_params = self._normalize_rack_params(
            dict(params or {}),
            fallback_code=str(entry.get("code") or "A"),
        )
        new_center_x = float(center[0])
        new_center_y = float(center[1])
        if anchor_world is not None:
            new_center_x, new_center_y = self._center_from_anchor_world_point(
                float(anchor_world[0]),
                float(anchor_world[1]),
                normalized_params,
                rotation,
            )
        for actor in entry.get("actors", []):
            try:
                self._plotter.remove_actor(actor)
            except Exception as _exc:
                log_exception(__name__, "update_selected_rack_params", _exc)
        actors, slot_actors = self._spawn_rack_actors(
            new_center_x, new_center_y, zone_id, normalized_params, rotation,
            *self._resolve_rack_style(normalized_params),
        )
        if not actors:
            return False
        hidden_slot_ids = set(str(v) for v in (entry.get("hidden_slot_ids") or set()))
        for sid in hidden_slot_ids:
            actor = slot_actors.get(sid)
            if actor is not None:
                try:
                    actor.SetVisibility(0)
                except Exception as _exc:
                    log_exception(__name__, "update_selected_rack_params", _exc)
        entry["params"] = normalized_params
        entry["code"] = str(normalized_params.get("code") or entry.get("code") or "A")
        entry["numbering_direction"] = str(
            normalized_params.get("numbering_direction")
            or entry.get("numbering_direction")
            or "left_to_right"
        )
        entry["span_codes"] = [str(v) for v in (normalized_params.get("span_codes") or entry.get("span_codes") or [])]
        entry["center"] = (new_center_x, new_center_y)
        entry["bbox"] = rack_bbox(new_center_x, new_center_y, normalized_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._rebuild_shelves_for_updated_rack_params(rack_id)
        if self._plotter:
            self._plotter.update()
        self.rack_layout_changed.emit(zone_id)
        self._set_selected_rack(rack_id)
        return True

    def can_update_selected_rack_params(
        self: "ModelViewWidget",
        params: dict[str, Any],
        anchor_mode: str | None = None,
    ) -> bool:
        entry = self.get_selected_rack_entry()
        if not entry:
            return False
        zone_id = str(entry.get("zone_id", ""))
        center = entry.get("center") or (0.0, 0.0)
        rotation = int(entry.get("rotation", 0)) % 360
        rack_id = str(entry.get("rack_id", ""))
        anchor_world = None
        if str(anchor_mode or "").strip().lower() == "back_numbering_side":
            anchor_world = self._rack_anchor_world_point(
                float(center[0]),
                float(center[1]),
                dict(entry.get("params") or {}),
                rotation,
            )
        normalized_params = self._normalize_rack_params(
            dict(params or {}),
            fallback_code=str(entry.get("code") or "A"),
        )
        new_center_x = float(center[0])
        new_center_y = float(center[1])
        if anchor_world is not None:
            new_center_x, new_center_y = self._center_from_anchor_world_point(
                float(anchor_world[0]),
                float(anchor_world[1]),
                normalized_params,
                rotation,
            )
        if not self._validate_rack_position(
            new_center_x,
            new_center_y,
            zone_id,
            normalized_params,
            rotation,
            ignore_rack_id=rack_id,
        ):
            return False
        return True


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Перемещение выбранного стеллажа и пересборка его состояния.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackMoveMixin: точки входа
#       Публичные методы сценария:
#       - RackMoveMixin.begin_move_selected_rack(...)
#       - RackMoveMixin.update_selected_rack_params(...)
#       - RackMoveMixin.can_update_selected_rack_params(...)
#
#    B. RackMoveMixin: запуск и настройка:
#       RackMoveMixin.begin_move_selected_rack(...)
#         Назначение: начинает move selected rack в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackMoveMixin._set_moving_rack_origin_ghost(...)
#       RackMoveMixin._set_moving_rack_origin_ghost(...)
#         Назначение: устанавливает moving rack origin ghost в рамках текущего сценария модуля.
#
#    C. RackMoveMixin: основной сценарий:
#       RackMoveMixin.update_selected_rack_params(...)
#         Назначение: обновляет selected rack params в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackMoveMixin.can_update_selected_rack_params(...)
#       RackMoveMixin.can_update_selected_rack_params(...)
#         Назначение: проверяет возможность update selected rack params в рамках текущего сценария модуля.
#
#    D. RackMoveMixin: завершение и очистка:
#       RackMoveMixin._restore_moving_rack_entry(...)
#         Назначение: Восстановить перемещённую стойку на исходную позицию при отмене.
#         Последовательность внутренних вызовов:
#         -> RackMoveMixin._set_moving_rack_origin_ghost(...)
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).

