# -*- coding: utf-8 -*-
# gui/components/model_view/_mv_racks_visual_models.py
"""Логика импорта / обнаружения моделей для пользовательских STL-моделей стеллажей."""

from __future__ import annotations

import math
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_WALL_BUFFER_MM
from error_logger import log_exception

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


class RackVisualModelsMixin:
    """Вспомогательные методы пользовательских моделей: импорт STL, определение опор, буферы."""

    def build_additional_model_params(self: "ModelViewWidget", model_path: Path) -> dict[str, Any] | None:
        mesh, width, depth, height = self._get_additional_model_base_mesh(Path(model_path))
        if mesh is None:
            return None
        width_mm = int(max(100, math.ceil(float(width) / 10.0) * 10))
        depth_mm = int(max(100, math.ceil(float(depth) / 10.0) * 10))
        height_mm = int(max(100, math.ceil(float(height) / 10.0) * 10))

        # Определение опор колонн и высоты просвета из меша.
        footings, clearance_h = self._detect_column_footings(mesh)
        if clearance_h > 0:
            height_mm = int(max(100, math.ceil(float(clearance_h) / 10.0) * 10))
        model_name = Path(model_path).name
        lower_name = model_name.lower()
        is_mezzanine = any(tag in lower_name for tag in ("mezo", "mezz", "мез", "mezan", "mezon"))

        return {
            "rack_type": "CUSTOM",
            "depth_mm": depth_mm,
            "spans_count": 1,
            "uniform_width": True,
            "span_widths_mm": [width_mm],
            "center_spans_mm": [width_mm],
            "footprint_width_mm": width_mm,
            "footprint_depth_mm": depth_mm,
            "footprint_height_mm": height_mm,
            "pillars_count": len(footings) if footings else 4,
            "additional_model": {
                "model_path": str(Path(model_path)),
                "model_name": model_name,
                "model_kind": "mezo" if is_mezzanine else "custom",
                "is_mezzanine": bool(is_mezzanine),
                "clearance_height_mm": height_mm,
                "column_footings": footings,
            },
        }

    def _build_additional_model_instance_mesh(
        self: "ModelViewWidget",
        cx: float,
        cy: float,
        z_ref: float,
        params: dict[str, Any],
        rotation: int,
    ):
        additional_model = dict(params.get("additional_model") or {})
        model_path = additional_model.get("model_path")
        if not model_path:
            return None
        base_mesh, _w, _d, _h = self._get_additional_model_base_mesh(Path(model_path))
        if base_mesh is None:
            return None
        try:
            mesh = base_mesh.copy(deep=True)
            mesh.rotate_z(float(int(rotation) % 360), point=(0.0, 0.0, 0.0), inplace=True)
            mesh.translate((float(cx), float(cy), float(z_ref)), inplace=True)
            return mesh
        except Exception as exc:
            log_exception(__name__, "_build_additional_model_instance_mesh", exc)
            return None

    def _get_additional_model_base_mesh(self: "ModelViewWidget", model_path: Path) -> tuple[Any, float, float, float]:
        if not _PV:
            return None, 0.0, 0.0, 0.0
        cache = getattr(self, "_additional_model_mesh_cache", {})
        key = str(Path(model_path).resolve())
        cached = cache.get(key)
        if cached is not None:
            return cached
        file_path = Path(model_path)
        if not file_path.is_absolute() and not file_path.exists():
            # Относительный путь из БД: пытаемся разрешить относительно корня проекта.
            racks_root = getattr(self, "_rack_models_root", None)
            if racks_root is not None:
                try:
                    project_root = Path(racks_root).resolve().parent.parent
                    file_path = project_root / file_path
                except Exception as _exc:
                    log_exception(__name__, "_get_additional_model_base_mesh", _exc)
        if not file_path.exists():
            cache[key] = (None, 0.0, 0.0, 0.0)
            self._additional_model_mesh_cache = cache
            return cache[key]
        try:
            mesh = pv.read(str(file_path))
            if mesh is None:
                cache[key] = (None, 0.0, 0.0, 0.0)
                self._additional_model_mesh_cache = cache
                return cache[key]
            bounds = mesh.bounds
            width = max(10.0, float(bounds[1] - bounds[0]))
            depth = max(10.0, float(bounds[3] - bounds[2]))
            height = max(10.0, float(bounds[5] - bounds[4]))
            cx = (bounds[0] + bounds[1]) / 2.0
            cy = (bounds[2] + bounds[3]) / 2.0
            min_z = bounds[4]
            mesh.translate((-cx, -cy, -min_z), inplace=True)
            cache[key] = (mesh, width, depth, height)
            self._additional_model_mesh_cache = cache
            return cache[key]
        except Exception as exc:
            log_exception(__name__, "_get_additional_model_base_mesh", exc)
            cache[key] = (None, 0.0, 0.0, 0.0)
            self._additional_model_mesh_cache = cache
            return cache[key]

    # -- утилиты ---------------------------------------------------------------

    def _append_polydata_safe(self: "ModelViewWidget", parts: list[Any]):
        if not parts:
            return None
        try:
            return pv.append_polydata(parts)
        except Exception as exc:
            log_exception(__name__, "_append_polydata_safe", exc)
            merged = parts[0]
            for item in parts[1:]:
                try:
                    merged = merged.merge(item, merge_points=False)
                except Exception as _exc:
                    log_exception(__name__, "_append_polydata_safe", _exc)
            return merged

    def _rotate_local(self: "ModelViewWidget", x: float, y: float, rotation: int) -> tuple[float, float]:
        rot = int(rotation) % 360
        if rot == 90:
            return -y, x
        if rot == 180:
            return -x, -y
        if rot == 270:
            return y, -x
        return x, y

    # -- Анализ опор колонн для пользовательских STL-моделей ------------------

    def _detect_column_footings(
        self: "ModelViewWidget",
        base_mesh: Any,
    ) -> tuple[list[dict[str, float]], float]:
        """Определить позиции опор колонн и высоту просвета палубы из меша.

        Анализирует нижнюю геометрию меша, касающуюся основания, для поиска
        изолированных кластеров (оснований колонн / опор). Также определяет высоту
        просвета под нижней горизонтальной палубой между колоннами.

        Returns
        -------
        footings : list[dict]
            Каждый словарь содержит ключи ``cx``, ``cy``, ``x_min``, ``y_min``,
            ``x_max``, ``y_max`` (все в локальных координатах модели, начало
            в центре-снизу).
        clearance_h : float
            Высота (мм) от основания до нижней стороны самой низкой палубы,
            перекрывающей пространство между колоннами. ``0.0``, если не удаётся определить.
        """
        try:
            import numpy as np
        except ImportError:
            return [], 0.0
        if base_mesh is None or not hasattr(base_mesh, "points"):
            return [], 0.0

        pts = np.asarray(base_mesh.points)
        if len(pts) == 0:
            return [], 0.0

        z_min = float(pts[:, 2].min())
        z_max = float(pts[:, 2].max())
        total_h = z_max - z_min

        # --- 1. Собрать нижние точки (в пределах 20 мм от основания) ---------
        eps_z = 20.0
        bottom_mask = pts[:, 2] < (z_min + eps_z)
        bottom_xy = pts[bottom_mask][:, :2]
        if len(bottom_xy) < 3:
            return [], 0.0

        # --- 2. Кластеризация на основе сетки (cell_size = 200 мм) ----------
        cell_size = 200.0
        from collections import defaultdict

        cell_pts: dict[tuple[int, int], list[int]] = defaultdict(list)
        for idx in range(len(bottom_xy)):
            x, y = float(bottom_xy[idx, 0]), float(bottom_xy[idx, 1])
            key = (int(round(x / cell_size)), int(round(y / cell_size)))
            cell_pts[key].append(idx)

        if not cell_pts:
            return [], 0.0

        # Поиск объединений (Union-Find)
        parent: dict[tuple[int, int], tuple[int, int]] = {k: k for k in cell_pts}

        def _find(a: tuple[int, int]) -> tuple[int, int]:
            while parent[a] != a:
                parent[a] = parent[parent[a]]
                a = parent[a]
            return a

        def _union(a: tuple[int, int], b: tuple[int, int]) -> None:
            parent[_find(a)] = _find(b)

        for k in list(cell_pts.keys()):
            for dx in (-1, 0, 1):
                for dy in (-1, 0, 1):
                    n = (k[0] + dx, k[1] + dy)
                    if n in cell_pts:
                        _union(k, n)

        clusters: dict[tuple[int, int], list[int]] = defaultdict(list)
        for k, indices in cell_pts.items():
            clusters[_find(k)].extend(indices)

        # Для определения колонн требуется не менее 2 изолированных кластеров.
        if len(clusters) < 2:
            return [], 0.0

        footings: list[dict[str, float]] = []
        for indices in clusters.values():
            cxy = bottom_xy[indices]
            x_min_f = float(cxy[:, 0].min())
            x_max_f = float(cxy[:, 0].max())
            y_min_f = float(cxy[:, 1].min())
            y_max_f = float(cxy[:, 1].max())
            footings.append({
                "cx": round((x_min_f + x_max_f) / 2.0, 1),
                "cy": round((y_min_f + y_max_f) / 2.0, 1),
                "x_min": round(x_min_f, 1),
                "y_min": round(y_min_f, 1),
                "x_max": round(x_max_f, 1),
                "y_max": round(y_max_f, 1),
            })

        # --- 3. Определение высоты просвета ----------------------------------
        all_cx = [f["cx"] for f in footings]
        all_cy = [f["cy"] for f in footings]
        mid_x = (min(all_cx) + max(all_cx)) / 2.0
        mid_y = (min(all_cy) + max(all_cy)) / 2.0
        search_r = max(cell_size, total_h * 0.05)
        centre_mask = (
            (np.abs(pts[:, 0] - mid_x) < search_r)
            & (np.abs(pts[:, 1] - mid_y) < search_r)
        )
        centre_z = pts[centre_mask, 2] if centre_mask.any() else np.array([])
        clearance_h = 0.0
        if len(centre_z) > 0:
            lowest_centre_z = float(centre_z.min())
            clearance_h = max(0.0, lowest_centre_z - z_min)

        return footings, clearance_h

    def _build_footing_buffers_mesh(
        self: "ModelViewWidget",
        cx: float,
        cy: float,
        z_ref: float,
        params: dict[str, Any],
        rotation: int,
    ):
        """Построить единый внешний контур буферной зоны по крайним пяткам."""
        if not _PV:
            return None
        additional_model = dict(params.get("additional_model") or {})
        footings = additional_model.get("column_footings")
        if not footings:
            return None

        b = float(MIN_WALL_BUFFER_MM)
        x_min = min(float(ft.get("x_min", 0.0)) for ft in footings)
        x_max = max(float(ft.get("x_max", 0.0)) for ft in footings)
        y_min = min(float(ft.get("y_min", 0.0)) for ft in footings)
        y_max = max(float(ft.get("y_max", 0.0)) for ft in footings)

        inner_w = max(10.0, x_max - x_min)
        inner_d = max(10.0, y_max - y_min)
        local_cx = (x_min + x_max) / 2.0
        local_cy = (y_min + y_max) / 2.0
        outer_w = inner_w + 2.0 * b
        outer_d = inner_d + 2.0 * b
        strips = [
            (local_cx, local_cy + inner_d / 2.0 + b / 2.0, outer_w, b),
            (local_cx, local_cy - (inner_d / 2.0 + b / 2.0), outer_w, b),
            (local_cx + inner_w / 2.0 + b / 2.0, local_cy, b, outer_d),
            (local_cx - (inner_w / 2.0 + b / 2.0), local_cy, b, outer_d),
        ]
        parts: list[Any] = []
        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) if parts else None


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Дополнительные модели стеллажей и буферы оснований и опор.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackVisualModelsMixin: точки входа
#       Публичные методы сценария:
#       - RackVisualModelsMixin.build_additional_model_params(...)
#
#    B. RackVisualModelsMixin: вспомогательные расчёты:
#       RackVisualModelsMixin.build_additional_model_params(...)
#         Назначение: строит additional model params в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackVisualModelsMixin._get_additional_model_base_mesh(...)
#         -> RackVisualModelsMixin._detect_column_footings(...)
#       RackVisualModelsMixin._build_additional_model_instance_mesh(...)
#         Назначение: строит additional model instance mesh в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackVisualModelsMixin._get_additional_model_base_mesh(...)
#       RackVisualModelsMixin._get_additional_model_base_mesh(...)
#         Назначение: возвращает additional model base mesh в рамках текущего сценария модуля.
#       RackVisualModelsMixin._append_polydata_safe(...)
#         Назначение: добавляет polydata safe в рамках текущего сценария модуля.
#       RackVisualModelsMixin._rotate_local(...)
#         Назначение: выполняет шаг "rotate local" в рамках текущего сценария модуля.
#       RackVisualModelsMixin._detect_column_footings(...)
#         Назначение: Определить позиции опор колонн и высоту просвета палубы из меша.
#       RackVisualModelsMixin._build_footing_buffers_mesh(...)
#         Назначение: Построить буферные контурные объёмы вокруг каждой опоры колонны.
#         Последовательность внутренних вызовов:
#         -> RackVisualModelsMixin._append_polydata_safe(...)
#         -> RackVisualModelsMixin._rotate_local(...)
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Геометрическая визуализация зависит от pyvista/vtk; при недоступности модуль обязан завершать шаг без падения сценария.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
