# -*- coding: utf-8 -*-
"""Вспомогательные функции предпросмотра / размещения стоек для ModelViewWidget."""

from __future__ import annotations

import uuid
from typing import Any, TYPE_CHECKING

from ._mv_rack_geometry import rack_bbox
from error_logger import log_exception

if TYPE_CHECKING:
    from ..model_view_widget import ModelViewWidget


class RackPreviewMixin:
    """Построение, перемещение, подтверждение и очистка предпросмотра стоек."""

    def _rebuild_rack_preview(self: "ModelViewWidget", cx: float, cy: float) -> None:
        prev_center = self._rack_preview_center
        zone_id = self._rack_preview_zone_id
        if not zone_id:
            self._clear_rack_preview()
            self._rack_preview_center = (cx, cy)
            return
        params = self._rack_preview_params
        valid = self._validate_rack_position(cx, cy, zone_id, params, self._rack_preview_rotation)
        self._rack_preview_valid = valid
        shape_key = self._preview_shape_key(params, self._rack_preview_rotation)

        if (
            self._rack_preview_actors
            and prev_center is not None
            and self._rack_preview_shape_key == shape_key
        ):
            dx = float(cx - prev_center[0])
            dy = float(cy - prev_center[1])
            if abs(dx) > 0.0 or abs(dy) > 0.0:
                self._translate_preview_actors(dx, dy)
            self._set_preview_color(valid)
            self._rack_preview_center = (cx, cy)
            if self._plotter:
                if hasattr(self, "_safe_render"):
                    self._safe_render(min_interval_s=1.0 / 75.0)
                else:
                    self._plotter.update()
            return

        disable_render = getattr(self._plotter, "disable_render", None) if self._plotter else None
        enable_render = getattr(self._plotter, "enable_render", None) if self._plotter else None
        if callable(disable_render):
            try:
                disable_render()
            except Exception as exc:
                log_exception(__name__, "_rebuild_rack_preview", exc)
                disable_render = None
        try:
            self._clear_rack_preview()
            color = "#4CAF50" if valid else "#EF5350"
            actors, _slot_actors = self._spawn_rack_actors(
                cx,
                cy,
                zone_id,
                params,
                self._rack_preview_rotation,
                color,
                0.35,
                lightweight=False,
            )
            self._rack_preview_actors = actors
            self._rack_preview_shape_key = shape_key
            self._rack_preview_center = (cx, cy)
        finally:
            if callable(enable_render):
                try:
                    enable_render()
                except Exception as _exc:
                    log_exception(__name__, "_rebuild_rack_preview", _exc)
        if self._plotter:
            if hasattr(self, "_safe_render"):
                self._safe_render(min_interval_s=1.0 / 75.0)
            else:
                self._plotter.update()

    def _commit_rack_placement(self: "ModelViewWidget", cx: float, cy: float) -> None:
        zone_id = self._rack_preview_zone_id
        if not zone_id:
            return
        params = dict(self._rack_preview_params)
        if self._moving_rack_entry:
            params = self._normalize_rack_params(
                params,
                fallback_code=str(dict(self._moving_rack_entry).get("code") or "A"),
            )
        else:
            params = self._normalize_rack_params(
                params,
                fallback_code=self._next_zone_rack_code(zone_id, params),
            )
        actors, slot_actors = self._spawn_rack_actors(
            cx,
            cy,
            zone_id,
            params,
            self._rack_preview_rotation,
            *self._resolve_rack_style(params),
        )
        if not actors:
            return
        bbox = rack_bbox(cx, cy, params, self._rack_preview_rotation)
        moved_committed = False
        if self._moving_rack_entry:
            moved = dict(self._moving_rack_entry)
            rack_id = str(moved.get("rack_id") or "")
            for actor in list(moved.get("actors") or []):
                try:
                    self._plotter.remove_actor(actor)
                except Exception as _exc:
                    log_exception(__name__, "_commit_rack_placement", _exc)
            if rack_id:
                self._clear_slot_shelf_actors(rack_id, clear_slot_id=None)
            moved["zone_id"] = zone_id
            moved["center"] = (cx, cy)
            moved["rotation"] = self._rack_preview_rotation
            moved["bbox"] = bbox
            moved["params"] = params
            moved["actors"] = actors
            moved["slot_actors"] = slot_actors
            moved["hidden_slot_ids"] = set(str(v) for v in (moved.get("hidden_slot_ids") or set()))
            moved["code"] = str(params.get("code") or moved.get("code") or "A")
            moved["name"] = str(params.get("name") or moved.get("name") or "Rack")
            moved["numbering_direction"] = str(
                params.get("numbering_direction")
                or moved.get("numbering_direction")
                or "left_to_right"
            )
            moved["span_codes"] = [str(v) for v in (params.get("span_codes") or moved.get("span_codes") or [])]
            moved.pop("_origin_rack_visual_snapshot", None)
            moved.pop("_origin_shelf_visual_snapshot", None)
            self._rack_entries.append(moved)
            self._moving_rack_entry = None
            if rack_id:
                self._rebuild_shelves_for_moved_rack(rack_id)
                self._last_moved_rack_id = rack_id
            moved_committed = True
        else:
            # Защита: режим перемещения не должен создавать новые стойки.
            if bool(getattr(self, "_rack_move_mode", False)):
                self._clear_rack_preview()
                return
            self._rack_entries.append(
                {
                    "rack_id": str(uuid.uuid4()),
                    "zone_id": zone_id,
                    "center": (cx, cy),
                    "rotation": self._rack_preview_rotation,
                    "bbox": bbox,
                    "params": params,
                    "actors": actors,
                    "slot_actors": slot_actors,
                    "hidden_slot_ids": set(),
                    "code": str(params.get("code") or "A"),
                    "name": str(params.get("name") or "Rack"),
                    "numbering_direction": str(params.get("numbering_direction") or "left_to_right"),
                    "span_codes": [str(v) for v in (params.get("span_codes") or [])],
                }
            )
        self._clear_rack_preview()
        if self._selected_rack_id:
            self._apply_selected_rack_visual(str(self._selected_rack_id))
            self.rack_selected_changed.emit(self._selected_rack_id)
        self.rack_layout_changed.emit(str(zone_id))
        if moved_committed:
            # Завершить перемещение после одного успешного размещения.
            self.stop_rack_placement(clear_preview=True)
            # Автоматически вернуться в режим выбора стоек для этой зоны.
            self.start_select_rack_mode(str(zone_id))

    def _rebuild_shelves_for_moved_rack(self: "ModelViewWidget", rack_id: str) -> None:
        rid = str(rack_id or "")
        if not rid:
            return
        slot_keys = [
            (entry_rid, slot_id)
            for (entry_rid, slot_id) in list((self._rack_shelf_params or {}).keys())
            if str(entry_rid) == rid
        ]
        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(rid, slot_id, payload)
            if not normalized:
                continue
            self._rack_shelf_params[(rid, slot_id)] = dict(normalized)
            self.set_rack_slot_occupied(rid, slot_id, True)
        self._set_rack_shelf_actors_visibility(rid, True)
        self._apply_rack_shelf_opacity(rid)
        if self._plotter:
            self._plotter.update()

    def _rebuild_shelves_for_updated_rack_params(self: "ModelViewWidget", rack_id: str) -> None:
        """Перерисовать существующие ряды полок после обновления геометрии/параметров стойки."""
        rid = str(rack_id or "")
        if not rid:
            return
        entry = self._get_rack_entry(rid)
        if entry is None:
            return

        available_slot_ids = set(str(v) for v in (entry.get("slot_actors") or {}).keys())
        slot_keys = [
            (entry_rid, slot_id)
            for (entry_rid, slot_id) in list((self._rack_shelf_params or {}).keys())
            if str(entry_rid) == rid
        ]
        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
            if slot_id not in available_slot_ids:
                # Ячейка удалена (например, уменьшилось spans_count) — удалить устаревшие данные/акторы полок.
                self._clear_slot_shelf_actors(rid, slot_id)
                self._rack_shelf_params.pop((rid, slot_id), None)
                continue
            normalized = self._render_shelves_for_slot(rid, slot_id, payload)
            if not normalized:
                continue
            self._rack_shelf_params[(rid, slot_id)] = dict(normalized)
            self.set_rack_slot_occupied(rid, slot_id, True)

        self._set_rack_shelf_actors_visibility(rid, True)
        self._apply_rack_shelf_opacity(rid)
        if self._plotter:
            self._plotter.update()

    def consume_last_moved_rack_id(self: "ModelViewWidget") -> str:
        rack_id = str(self._last_moved_rack_id or "")
        self._last_moved_rack_id = None
        return rack_id

    def _clear_rack_preview(self: "ModelViewWidget") -> None:
        for actor in self._rack_preview_actors:
            try:
                self._plotter.remove_actor(actor)
            except Exception as _exc:
                log_exception(__name__, "_clear_rack_preview", _exc)
        self._rack_preview_actors = []
        self._rack_preview_shape_key = None

    def _preview_shape_key(self: "ModelViewWidget", params: dict[str, Any], rotation: int) -> tuple[Any, ...]:
        return (
            int(params.get("footprint_width_mm", 1000)),
            int(params.get("footprint_depth_mm", 500)),
            int(params.get("footprint_height_mm", 1800)),
            int(rotation) % 360,
        )

    def _translate_preview_actors(self: "ModelViewWidget", dx: float, dy: float) -> None:
        for actor in self._rack_preview_actors:
            try:
                actor.AddPosition(float(dx), float(dy), 0.0)
            except Exception as exc:
                log_exception(__name__, "_translate_preview_actors", exc)
                try:
                    px, py, pz = actor.GetPosition()
                    actor.SetPosition(float(px + dx), float(py + dy), float(pz))
                except Exception as _exc:
                    log_exception(__name__, "_translate_preview_actors", _exc)
    def _set_preview_color(self: "ModelViewWidget", valid: bool) -> None:
        if not self._rack_preview_actors:
            return
        rgb = (0.298, 0.686, 0.314) if valid else (0.937, 0.325, 0.314)
        try:
            prop = self._rack_preview_actors[0].GetProperty()
            if prop is not None:
                prop.SetColor(*rgb)
        except Exception as _exc:
            log_exception(__name__, "_set_preview_color", _exc)
# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Предпросмотр стеллажа и фиксация размещения.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackPreviewMixin: точки входа
#       Публичные методы сценария:
#       - RackPreviewMixin.consume_last_moved_rack_id(...)
#
#    B. RackPreviewMixin: запуск и настройка:
#       RackPreviewMixin._set_preview_color(...)
#         Назначение: устанавливает preview color в рамках текущего сценария модуля.
#
#    C. RackPreviewMixin: основной сценарий:
#       RackPreviewMixin._rebuild_rack_preview(...)
#         Назначение: перестраивает rack preview в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPreviewMixin._preview_shape_key(...)
#         -> RackPreviewMixin._clear_rack_preview(...)
#         -> RackPreviewMixin._set_preview_color(...)
#         -> RackPreviewMixin._translate_preview_actors(...)
#       RackPreviewMixin._rebuild_shelves_for_moved_rack(...)
#         Назначение: перестраивает shelves for moved rack в рамках текущего сценария модуля.
#       RackPreviewMixin._rebuild_shelves_for_updated_rack_params(...)
#         Назначение: Перерисовать существующие ряды полок после обновления геометрии/параметров стойки.
#
#    D. RackPreviewMixin: завершение и очистка:
#       RackPreviewMixin._clear_rack_preview(...)
#         Назначение: очищает rack preview в рамках текущего сценария модуля.
#
#    E. RackPreviewMixin: вспомогательные расчёты:
#       RackPreviewMixin._commit_rack_placement(...)
#         Назначение: выполняет шаг "commit rack placement" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPreviewMixin._clear_rack_preview(...)
#         -> RackPreviewMixin._rebuild_shelves_for_moved_rack(...)
#       RackPreviewMixin.consume_last_moved_rack_id(...)
#         Назначение: выполняет шаг "consume last moved rack id" в рамках текущего сценария модуля.
#       RackPreviewMixin._preview_shape_key(...)
#         Назначение: выполняет шаг "preview shape key" в рамках текущего сценария модуля.
#       RackPreviewMixin._translate_preview_actors(...)
#         Назначение: выполняет шаг "translate preview actors" в рамках текущего сценария модуля.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
