# -*- coding: utf-8 -*-
# gui/components/model_view/_mv_racks_io.py
"""IO-операции для восстановление/сериализации размещённых стоек."""

from __future__ import annotations

import uuid
from pathlib import Path
from typing import Any, Callable, TYPE_CHECKING

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

if TYPE_CHECKING:  # pragma: no cover
    from gui.components.model_view_widget import ModelViewWidget


class RackPlacementIOMixin:
    """Mixin IO-операций для RackPlacementMixin."""

    def get_zone_rack_layout(self: "ModelViewWidget", zone_id: str) -> list[dict[str, Any]]:
        zid = str(zone_id or "")
        payload: list[dict[str, Any]] = []
        for entry in self._rack_entries:
            if entry.get("zone_id") != zid:
                continue
            params = dict(entry.get("params") or {})
            if hasattr(self, "_normalize_rack_params"):
                try:
                    params = self._normalize_rack_params(
                        params,
                        fallback_code=str(entry.get("code") or params.get("code") or "A"),
                    )
                except Exception as _exc:
                    log_exception(__name__, "get_zone_rack_layout", _exc)
            center = entry.get("center") or (0.0, 0.0)
            payload.append(
                {
                    "rack_id": str(entry.get("rack_id") or str(uuid.uuid4())),
                    "code": str(entry.get("code") or params.get("code") or "A"),
                    "name": str(entry.get("name") or "Rack"),
                    "created_ts": str(entry.get("created_ts") or ""),
                    "rack_type": str(params.get("rack_type", "A")),
                    "depth_mm": int(params.get("depth_mm", 500)),
                    "spans_count": int(params.get("spans_count", 1)),
                    "numbering_direction": str(
                        params.get("numbering_direction")
                        or entry.get("numbering_direction")
                        or "left_to_right"
                    ),
                    "span_codes": [
                        str(v)
                        for v in (
                            params.get("span_codes")
                            or entry.get("span_codes")
                            or []
                        )
                    ],
                    "uniform_width": bool(params.get("uniform_width", True)),
                    "span_widths_mm": [int(v) for v in (params.get("span_widths_mm") or [1000])],
                    "center_spans_mm": [int(v) for v in (params.get("center_spans_mm") or [1000])],
                    "footprint_width_mm": int(params.get("footprint_width_mm", 1000)),
                    "footprint_depth_mm": int(params.get("footprint_depth_mm", 500)),
                    "footprint_height_mm": int(params.get("footprint_height_mm", 1800)),
                    "pillars_count": int(params.get("pillars_count", 4)),
                    "center_x": float(center[0]),
                    "center_y": float(center[1]),
                    "rotation_deg": int(entry.get("rotation", 0)),
                    "hidden_slot_ids": sorted(str(v) for v in (entry.get("hidden_slot_ids") or set())),
                    "shelf_helpers_hidden": bool(entry.get("shelf_helpers_hidden", False)),
                    "display_color": str(params.get("display_color") or ""),
                    "display_opacity": float(params.get("display_opacity", 0.9)),
                    "additional_model": dict(params.get("additional_model") or {}),
                }
            )
        return payload

    def load_zone_rack_layout(
        self: "ModelViewWidget",
        zone_id: str,
        racks: list[dict[str, Any]],
        models_root: Path | None = None,
        progress_callback: Callable[[int, int], None] | None = None,
    ) -> None:
        zid = str(zone_id or "")
        self._rack_models_root = Path(models_root) if models_root else self._rack_models_root
        self._remove_zone_racks(zid)
        rack_items = list(racks or [])
        total_items = len(rack_items)
        for item_idx, item in enumerate(rack_items, start=1):
            try:
                if "center_x" not in item or "center_y" not in item:
                    continue
                params = {
                    "code": str(item.get("code") or "A"),
                    "rack_type": str(item.get("rack_type", "A")),
                    "depth_mm": int(item.get("depth_mm", 500)),
                    "spans_count": int(item.get("spans_count", 1)),
                    "numbering_direction": str(item.get("numbering_direction") or "left_to_right"),
                    "span_codes": [str(v) for v in (item.get("span_codes") or [])],
                    "uniform_width": bool(item.get("uniform_width", True)),
                    "span_widths_mm": [int(v) for v in (item.get("span_widths_mm") or [1000])],
                    "center_spans_mm": [int(v) for v in (item.get("center_spans_mm") or [1000])],
                    "footprint_width_mm": int(item.get("footprint_width_mm", 1000)),
                    "footprint_depth_mm": int(item.get("footprint_depth_mm", 500)),
                    "footprint_height_mm": int(item.get("footprint_height_mm", 1800)),
                    "pillars_count": int(item.get("pillars_count", 4)),
                    "display_color": str(item.get("display_color") or ""),
                    "display_opacity": float(item.get("display_opacity", 0.9)),
                    "additional_model": dict(item.get("additional_model") or {}),
                }
                if hasattr(self, "_normalize_rack_params"):
                    try:
                        params = self._normalize_rack_params(
                            params,
                            fallback_code=str(item.get("code") or "A"),
                        )
                    except Exception as _exc:
                        log_exception(__name__, "load_zone_rack_layout", _exc)
                cx = float(item.get("center_x", 0.0))
                cy = float(item.get("center_y", 0.0))
                rotation = int(item.get("rotation_deg", 0)) % 360
                color, opacity = self._resolve_rack_style(params)
                actors, slot_actors = self._spawn_rack_actors(cx, cy, zid, params, rotation, color, opacity)
                if not actors:
                    continue
                hidden_slot_ids = {str(v) for v in (item.get("hidden_slot_ids") or [])}
                for slot_id in hidden_slot_ids:
                    actor = slot_actors.get(slot_id)
                    if actor is not None:
                        try:
                            actor.SetVisibility(0)
                        except Exception as _exc:
                            log_exception(__name__, "load_zone_rack_layout", _exc)
                self._rack_entries.append(
                    {
                        "rack_id": str(item.get("rack_id") or str(uuid.uuid4())),
                        "zone_id": zid,
                        "center": (cx, cy),
                        "rotation": rotation,
                        "bbox": rack_bbox(cx, cy, params, rotation),
                        "params": params,
                        "actors": actors,
                        "slot_actors": slot_actors,
                        "hidden_slot_ids": hidden_slot_ids,
                        "shelf_helpers_hidden": bool(item.get("shelf_helpers_hidden", False)),
                        "code": str(item.get("code") or "A"),
                        "name": str(item.get("name") or "Rack"),
                        "created_ts": str(item.get("created_ts") or ""),
                        "numbering_direction": str(item.get("numbering_direction") or params.get("numbering_direction") or "left_to_right"),
                        "span_codes": [str(v) for v in (item.get("span_codes") or params.get("span_codes") or [])],
                    }
                )
                self._apply_rack_helper_visibility_policy(self._rack_entries[-1], True)
            finally:
                if progress_callback is not None:
                    try:
                        progress_callback(item_idx, total_items)
                    except Exception as _exc:
                        log_exception(__name__, "load_zone_rack_layout", _exc)
        if self._plotter:
            self._plotter.update()

    def _remove_zone_racks(self: "ModelViewWidget", zone_id: str) -> None:
        removed_rack_ids: set[str] = set()
        remaining: list[dict[str, Any]] = []
        for entry in self._rack_entries:
            if entry.get("zone_id") != zone_id:
                remaining.append(entry)
                continue
            rack_id = str(entry.get("rack_id") or "")
            if rack_id:
                removed_rack_ids.add(rack_id)
            for actor in entry.get("actors", []):
                try:
                    self._plotter.remove_actor(actor)
                except Exception as _exc:
                    log_exception(__name__, "_remove_zone_racks", _exc)
            if rack_id:
                self._clear_slot_shelf_actors(rack_id, clear_slot_id=None)
        self._rack_entries = remaining
        if not removed_rack_ids:
            return

        try:
            if str(getattr(self, "_selected_rack_id", "") or "") in removed_rack_ids:
                self._set_selected_rack(None)
        except Exception as _exc:
            log_exception(__name__, "_remove_zone_racks", _exc)
        try:
            if str(getattr(self, "_hover_rack_id", "") or "") in removed_rack_ids:
                self._clear_hover_rack_highlight()
        except Exception as _exc:
            log_exception(__name__, "_remove_zone_racks", _exc)
        try:
            if str(getattr(self, "_rack_isolation_rack_id", "") or "") in removed_rack_ids:
                self._rack_isolation_active = False
                self._rack_isolation_rack_id = None
                self._clear_rack_isolation_visual()
        except Exception as _exc:
            log_exception(__name__, "_remove_zone_racks", _exc)
        try:
            if str(getattr(self, "_shelf_target_rack_id", "") or "") in removed_rack_ids:
                self.stop_shelf_placement_ext(clear_selection=True, clear_rendered_shelves=True)
        except Exception as _exc:
            log_exception(__name__, "_remove_zone_racks", _exc)
        try:
            if str(getattr(self, "_selected_shelf_bbox_rack_id", "") or "") in removed_rack_ids:
                self._clear_selected_rendered_shelf(render=False)
        except Exception as _exc:
            log_exception(__name__, "_remove_zone_racks", _exc)
        try:
            if hasattr(self, "clear_cell_grid_visualization"):
                for rid in removed_rack_ids:
                    self.clear_cell_grid_visualization(rid)
        except Exception as _exc:
            log_exception(__name__, "_remove_zone_racks", _exc)
# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Сериализация и загрузка раскладок стеллажей по зонам.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackPlacementIOMixin: точки входа
#       Публичные методы сценария:
#       - RackPlacementIOMixin.get_zone_rack_layout(...)
#       - RackPlacementIOMixin.load_zone_rack_layout(...)
#
#    B. RackPlacementIOMixin: запуск и настройка:
#       RackPlacementIOMixin.load_zone_rack_layout(...)
#         Назначение: загружает zone rack layout в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPlacementIOMixin._remove_zone_racks(...)
#
#    C. RackPlacementIOMixin: завершение и очистка:
#       RackPlacementIOMixin._remove_zone_racks(...)
#         Назначение: удаляет zone racks в рамках текущего сценария модуля.
#
#    D. RackPlacementIOMixin: вспомогательные расчёты:
#       RackPlacementIOMixin.get_zone_rack_layout(...)
#         Назначение: возвращает zone rack layout в рамках текущего сценария модуля.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
