# -*- coding: utf-8 -*-
# gui/components/model_view/_mv_racks_visual.py
"""Визуализация стоек и вспомогательных объёмов в rack-placement."""

from __future__ import annotations

import math
import re
from pathlib import Path
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 (
    MIN_AISLE_MM,
    MIN_WALL_BUFFER_MM,
    support_line_positions,
)
from gui.components.model_view._mv_racks_visual_models import RackVisualModelsMixin

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


class RackPlacementVisualMixin(RackVisualModelsMixin):
    """Построение actor-ов стеллажа для предпросмотра и фиксации."""

    def _spawn_rack_actors(
        self: "ModelViewWidget", cx: float, cy: float, zone_id: str,
        params: dict[str, Any], rotation: int, color: str, opacity: float,
        lightweight: bool = False,
    ) -> tuple[list, dict[str, Any]]:
        if not _PV or not self._plotter:
            return [], {}
        z_ref = float(self._zone_heights.get(zone_id, (0.0, 0.0))[0])
        actors: list[Any] = []
        slot_actors: dict[str, Any] = {}
        additional_model = dict(params.get("additional_model") or {})

        def _add(m, c, o):
            return self._plotter.add_mesh(m, color=c, opacity=o, show_edges=False)

        if lightweight:
            if additional_model.get("model_path"):
                body = self._build_additional_model_instance_mesh(cx, cy, z_ref, params, rotation)
            else:
                body = self._build_lightweight_body_mesh(cx, cy, z_ref, params, rotation)
            if body is not None:
                actors.append(_add(body, color, opacity))
            if not additional_model.get("model_path"):
                for av in self._build_aisle_volumes(cx, cy, z_ref, params, rotation):
                    actors.append(_add(av, (1.0, 0.95, 0.10), max(0.55, opacity * 0.90)))
            return actors, {}

        if additional_model.get("model_path"):
            custom_mesh = self._build_additional_model_instance_mesh(cx, cy, z_ref, params, rotation)
            if custom_mesh is not None:
                actors.append(_add(custom_mesh, color, opacity))
            # Автоопределение опор, если они не сохранены в params (устаревшие данные).
            if not additional_model.get("column_footings"):
                bm, _w, _d, _h = self._get_additional_model_base_mesh(Path(additional_model["model_path"]))
                if bm is not None:
                    footings, _ch = self._detect_column_footings(bm)
                    if footings:
                        additional_model["column_footings"] = footings
                        params.setdefault("additional_model", {})["column_footings"] = footings
            footing_buffer = self._build_footing_buffers_mesh(cx, cy, z_ref, params, rotation)
            if footing_buffer is not None:
                actors.append(_add(footing_buffer, (0.10, 0.70, 1.00), max(0.75, opacity * 0.95)))
            return actors, {}

        positions = support_line_positions(cx, cy, params, rotation)
        pillars_mesh, pillar_height = self._build_pillars_mesh(positions, z_ref, params, rotation)
        if pillars_mesh is not None:
            actors.append(_add(pillars_mesh, color, opacity))
        for slot_id, mesh in self._build_rack_inner_slot_meshes(
            cx, cy, z_ref, params, rotation, pillar_height,
        ):
            actor = _add(mesh, (0.45, 0.65, 0.95), max(0.12, opacity * 0.45))
            slot_actors[slot_id] = actor
            actors.append(actor)
        for av in self._build_aisle_volumes(cx, cy, z_ref, params, rotation):
            actors.append(_add(av, (1.0, 0.95, 0.10), max(0.55, opacity * 0.90)))
        buffer_mesh = self._build_buffer_mesh(cx, cy, z_ref, params, rotation)
        if buffer_mesh is not None:
            actors.append(_add(buffer_mesh, (0.10, 0.70, 1.00), max(0.75, opacity * 0.95)))
        return actors, slot_actors

    # -- облегчённые / стоечные меши -----------------------------------------
    def _build_lightweight_body_mesh(
        self: "ModelViewWidget", cx: float, cy: float, z_ref: float,
        params: dict[str, Any], rotation: int,
    ):
        if not _PV:
            return None
        width = float(params.get("footprint_width_mm", 1000))
        depth = float(params.get("footprint_depth_mm", 500))
        height = max(100.0, float(params.get("footprint_height_mm", 1800)))
        if int(rotation) % 180 == 90:
            width, depth = depth, width
        return pv.Cube(
            center=(float(cx), float(cy), float(z_ref) + height / 2.0),
            x_length=max(10.0, width), y_length=max(10.0, depth), z_length=height,
        )

    def _build_pillars_mesh(
        self: "ModelViewWidget", positions: list[tuple[float, float]],
        z_ref: float, params: dict[str, Any], rotation: int = 0,
    ) -> tuple[Any, float]:
        fallback_height = max(100.0, float(params.get("footprint_height_mm", 1800)))
        if not _PV or not positions:
            return None, fallback_height
        base_mesh, pillar_height = self._get_pillar_base_mesh(params)
        if base_mesh is None:
            parts = [
                pv.Cube(
                    center=(px, py, z_ref + fallback_height / 2.0),
                    x_length=70.0, y_length=70.0, z_length=fallback_height,
                )
                for px, py in positions
            ]
            return self._append_polydata_safe(parts), fallback_height
        parts = []
        for px, py in positions:
            item = base_mesh.copy(deep=True)
            item.rotate_z(float(int(rotation) % 360), point=(0.0, 0.0, 0.0), inplace=True)
            item.translate((px, py, z_ref), inplace=True)
            parts.append(item)
        return self._append_polydata_safe(parts), float(pillar_height)

    def _get_pillar_base_mesh(self: "ModelViewWidget", params: dict[str, Any]) -> tuple[Any, float]:
        if not _PV:
            return None, 1800.0
        rack_type = str(params.get("rack_type", "A")).upper()
        depth = int(params.get("depth_mm", 500))
        key = (rack_type, depth)
        cache = getattr(self, "_pillar_mesh_cache", {})
        if key in cache:
            return cache[key]
        def _put(v):
            cache[key] = v; self._pillar_mesh_cache = cache; return v
        path = self._find_pillar_model_path(rack_type, depth)
        if path is None or not path.exists():
            return _put((None, 1800.0))
        try:
            mesh = pv.read(str(path))
            b = mesh.bounds
            mesh.translate((-((b[0]+b[1])/2), -((b[2]+b[3])/2), -b[4]), inplace=True)
            return _put((mesh, max(100.0, float(b[5] - b[4]))))
        except Exception as exc:
            log_exception(__name__, "_get_pillar_base_mesh", exc)
            return _put((None, 1800.0))

    def _find_pillar_model_path(self: "ModelViewWidget", rack_type: str, depth: int) -> Path | None:
        root = getattr(self, "_rack_models_root", None)
        if root is None:
            return None
        folder = Path(root) / f"pillars_type_{str(rack_type).lower()}"
        if not folder.exists():
            return None
        token = re.escape(str(rack_type).lower())
        pat = re.compile(rf"pillar_type_{token}_(\d+)_(\d+)\.stl$", re.IGNORECASE)
        best_path: Path | None = None
        best_diff: int | None = None
        for fp in folder.glob("*.stl"):
            m = pat.search(fp.name)
            if not m:
                continue
            diff = abs(int(m.group(1)) - int(depth))
            if best_diff is None or diff < best_diff:
                best_diff, best_path = diff, fp
                if diff == 0:
                    break
        return best_path

    # -- внутренние секции / проход / буфер -----------------------------------
    def _build_rack_inner_slot_meshes(
        self: "ModelViewWidget", cx: float, cy: float, z_ref: float,
        params: dict[str, Any], rotation: int, pillar_height: float,
    ) -> list[tuple[str, Any]]:
        if not _PV:
            return []
        depth = float(params.get("footprint_depth_mm", 500))
        center_spans = [int(v) for v in (params.get("center_spans_mm") or []) if int(v) > 0]
        if not center_spans:
            center_spans = [int(params.get("footprint_width_mm", 1000))]
        z_len = max(100.0, float(pillar_height))
        z_center = z_ref + z_len / 2.0
        y_len = max(10.0, depth - 60.0)
        if str(params.get("rack_type") or "").strip().lower() == "pallet":
            z_len = max(100.0, float(params.get("footprint_height_mm", z_len)))
            z_center = z_ref + z_len / 2.0
            y_len = max(10.0, float(params.get("footprint_depth_mm", 1100)))
            total_width = float(params.get("footprint_width_mm", sum(center_spans)))
            x_len = max(10.0, total_width)
            if int(rotation) % 180 == 90:
                x_len, y_len = y_len, x_len
            mesh = pv.Cube(
                center=(float(cx), float(cy), z_center),
                x_length=x_len, y_length=y_len, z_length=z_len,
            )
            return [("S1", mesh)]
        total_width = float(sum(center_spans))
        x_cursor = -total_width / 2.0
        result: list[tuple[str, Any]] = []
        for idx, span in enumerate(center_spans, start=1):
            span_len = max(10.0, float(span) - 60.0)
            local_x = x_cursor + float(span) / 2.0
            world_dx, world_dy = self._rotate_local(local_x, 0.0, rotation)
            xl, yl = span_len, y_len
            if int(rotation) % 180 == 90:
                xl, yl = yl, xl
            mesh = pv.Cube(
                center=(cx + world_dx, cy + world_dy, z_center),
                x_length=xl, y_length=yl, z_length=z_len,
            )
            result.append((f"S{idx}", mesh))
            x_cursor += float(span)
        return result

    def _build_aisle_volumes(
        self: "ModelViewWidget", cx: float, cy: float, z_ref: float,
        params: dict[str, Any], rotation: int,
    ) -> list[Any]:
        if not _PV:
            return []
        w = float(params.get("footprint_width_mm", 1000))
        d = float(params.get("footprint_depth_mm", 500))
        try:
            a = float(self._rack_access_aisle_mm(params))
        except Exception as exc:
            log_exception(__name__, "_build_aisle_volumes", exc)
            a = float(MIN_AISLE_MM)
        outer_left = -(w / 2.0 + a)
        outer_right = (w / 2.0 + a)
        y_near = d / 2.0
        y_far = d / 2.0 + a

        radius = min(900.0, a, (outer_right - outer_left) * 0.5)
        arc_segments = 16

        points_local: list[tuple[float, float]] = []
        points_local.append((outer_left, y_near))
        points_local.append((outer_right, y_near))
        points_local.append((outer_right, y_far - radius))

        if radius > 1.0:
            cx_r = outer_right - radius
            cy_r = y_far - radius
            for i in range(1, arc_segments + 1):
                t = i / float(arc_segments)
                ang = 0.0 + (1.5707963267948966 * t)
                points_local.append((cx_r + radius * math.cos(ang), cy_r + radius * math.sin(ang)))
            points_local.append((outer_left + radius, y_far))
            cx_l = outer_left + radius
            cy_l = y_far - radius
            for i in range(1, arc_segments + 1):
                t = i / float(arc_segments)
                ang = 1.5707963267948966 + (1.5707963267948966 * t)
                points_local.append((cx_l + radius * math.cos(ang), cy_l + radius * math.sin(ang)))
        else:
            points_local.append((outer_right, y_far))
            points_local.append((outer_left, y_far))

        points_local.append((outer_left, y_near))

        base_z = float(z_ref) + 1.0
        points_world = []
        for lx, ly in points_local:
            wx, wy = self._rotate_local(float(lx), float(ly), rotation)
            points_world.append((float(cx + wx), float(cy + wy), base_z))

        try:
            face = [len(points_world)] + list(range(len(points_world)))
            poly = pv.PolyData(points_world, faces=face)
            mesh = poly.triangulate().extrude((0.0, 0.0, 5.0), capping=True)
            if radius > 1.0:
                # Для PALLET _rack_access_aisle_mm(params) возвращает 2300 мм.
                # Используем то же значение для боковых доборов, чтобы зона
                # доступа была консистентна по глубине и выносу.
                side_access_mm = max(0.0, float(a))
                rear_limit_y = -d / 2.0
                tabs: list[Any] = []
                # Боковые доборы pallet: вдоль всей короткой стороны стеллажа
                # (по глубине), с выносом по X от боковой грани на side_access_mm.
                tab_rects = [
                    [
                        (w / 2.0, rear_limit_y),
                        (w / 2.0 + side_access_mm, rear_limit_y),
                        (w / 2.0 + side_access_mm, y_near),
                        (w / 2.0, y_near),
                    ],
                    [
                        (-w / 2.0 - side_access_mm, rear_limit_y),
                        (-w / 2.0, rear_limit_y),
                        (-w / 2.0, y_near),
                        (-w / 2.0 - side_access_mm, y_near),
                    ],
                ]
                for rect in tab_rects:
                    rect_world = []
                    for lx, ly in rect:
                        wx, wy = self._rotate_local(float(lx), float(ly), rotation)
                        rect_world.append((float(cx + wx), float(cy + wy), base_z))
                    f = [len(rect_world)] + list(range(len(rect_world)))
                    tabs.append(pv.PolyData(rect_world, faces=f).triangulate().extrude((0.0, 0.0, 5.0), capping=True))
                merged = self._append_polydata_safe([mesh] + tabs)
                if merged is not None:
                    mesh = merged
            return [mesh]
        except Exception as exc:
            log_exception(__name__, "_build_aisle_volumes", exc)
            return []

    def _build_buffer_mesh(
        self: "ModelViewWidget", cx: float, cy: float, z_ref: float,
        params: dict[str, Any], rotation: int,
    ):
        if not _PV:
            return None
        depth = max(10.0, float(params.get("footprint_depth_mm", 500)))
        b = float(MIN_WALL_BUFFER_MM)
        center_spans = [float(v) for v in (params.get("center_spans_mm") or []) if float(v) > 0.0]
        if center_spans:
            inner_w = max(10.0, float(sum(center_spans)) + 70.0)
        else:
            inner_w = max(10.0, float(params.get("footprint_width_mm", 1000)))
        inner_d = depth
        outer_w = inner_w + 2.0 * b
        outer_d = inner_d + 2.0 * b
        tx = max(10.0, b)
        ty = max(10.0, b)
        strips = [
            (0.0, inner_d / 2.0 + ty / 2.0, outer_w, ty),
            (0.0, -(inner_d / 2.0 + ty / 2.0), outer_w, ty),
            (inner_w / 2.0 + tx / 2.0, 0.0, tx, outer_d),
            (-(inner_w / 2.0 + tx / 2.0), 0.0, tx, outer_d),
        ]
        parts = []
        for lx, ly, xl, yl in strips:
            wx, wy = self._rotate_local(lx, ly, rotation)
            if int(rotation) % 180 == 90:
                xl, yl = yl, xl
            parts.append(pv.Cube(
                center=(cx + wx, cy + wy, z_ref + 5.0),
                x_length=max(10.0, xl), y_length=max(10.0, yl), z_length=10.0))
        return self._append_polydata_safe(parts)


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Построение базовой визуальной геометрии стеллажей и служебных объёмов.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackPlacementVisualMixin: точки входа
#       Публичные методы отсутствуют; сценарий запускается через методы родительских модулей и внутренние обработчики.
#
#    B. RackPlacementVisualMixin: основной сценарий:
#       RackPlacementVisualMixin._find_pillar_model_path(...)
#         Назначение: находит pillar model path в рамках текущего сценария модуля.
#
#    C. RackPlacementVisualMixin: вспомогательные расчёты:
#       RackPlacementVisualMixin._spawn_rack_actors(...)
#         Назначение: выполняет шаг "spawn rack actors" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPlacementVisualMixin._build_pillars_mesh(...)
#         -> RackPlacementVisualMixin._build_rack_inner_slot_meshes(...)
#         -> RackPlacementVisualMixin._build_aisle_volumes(...)
#         -> RackPlacementVisualMixin._build_buffer_mesh(...)
#         -> RackPlacementVisualMixin._build_lightweight_body_mesh(...)
#       RackPlacementVisualMixin._build_lightweight_body_mesh(...)
#         Назначение: строит lightweight body mesh в рамках текущего сценария модуля.
#       RackPlacementVisualMixin._build_pillars_mesh(...)
#         Назначение: строит pillars mesh в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPlacementVisualMixin._get_pillar_base_mesh(...)
#       RackPlacementVisualMixin._get_pillar_base_mesh(...)
#         Назначение: возвращает pillar base mesh в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackPlacementVisualMixin._find_pillar_model_path(...)
#       RackPlacementVisualMixin._build_rack_inner_slot_meshes(...)
#         Назначение: строит rack inner slot meshes в рамках текущего сценария модуля.
#       RackPlacementVisualMixin._build_aisle_volumes(...)
#         Назначение: строит aisle volumes в рамках текущего сценария модуля.
#       RackPlacementVisualMixin._build_buffer_mesh(...)
#         Назначение: строит buffer mesh в рамках текущего сценария модуля.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Геометрическая визуализация зависит от pyvista/vtk; при недоступности модуль обязан завершать шаг без падения сценария.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
