# -*- coding: utf-8 -*-
# gui/components/model_view/_mv_grid_core_base.py
# Основные методы сетки: тема, жизненный цикл, видимость, узлы, плоскость Z

from __future__ import annotations

import hashlib
import json
import math
from pathlib import Path
from typing import Optional, Tuple, TYPE_CHECKING, Callable

from error_logger import log_exception

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

try:
    import pyvista as pv
except ImportError:  # pragma: no cover
    pv = None


class GridCoreBaseMixin:
    """Сетка на полу: тема, создание, видимость, кэш, узлы."""

    def _get_grid_color(self: "ModelViewWidget") -> tuple[int, int, int]:
        theme = (getattr(self, "_theme", "dark") or "dark").lower()
        if theme == "dark":
            return (0, 0, 0)
        return (180, 180, 180)

    def _apply_grid_theme(self: "ModelViewWidget") -> None:
        """Перекрасить акторы сетки в соответствии с текущей темой."""
        color = self._get_grid_color()
        rgb = (color[0] / 255.0, color[1] / 255.0, color[2] / 255.0)
        actors = []
        actors.extend(getattr(self, "_grid_meshes", []))
        actors.extend(getattr(self, "_grid_surface_meshes", []))
        for actor in actors:
            try:
                prop = actor.GetProperty()
                if prop:
                    prop.SetColor(*rgb)
            except Exception as e:
                log_exception(__name__, "_apply_grid_theme.set_color", e)
        try:
            if self._plotter:
                self._plotter.update()
        except Exception as e:
            log_exception(__name__, "_apply_grid_theme.plotter_update", e)

    def start_zone_selection_grid(
        self: "ModelViewWidget",
        cell_size: int,
        grid_origin: Optional[Tuple[float, float, float]] = None,
        progress_callback: Optional[Callable[[int], None]] = None,
        z_size: Optional[int] = None,
    ):
        """Включить режим выбора зоны через сетку на полу.

        Если сетка с такими же параметрами уже построена (``_grid_ready``),
        повторное построение пропускается — переиспользуются существующие
        акторы и данные ячеек.
        """
        if not self._models_loaded or not self._plotter:
            return

        if self._floor_mesh is not None:
            min_x, max_x, min_y, max_y, min_z, _ = self._floor_mesh.bounds
        elif self._room_bounds:
            min_x, max_x, min_y, max_y, min_z, _ = self._room_bounds
        else:
            return

        z_size_int = int(z_size) if z_size is not None else int(cell_size)

        if grid_origin is not None:
            self._grid_origin = grid_origin
        grid_z = self._get_grid_plane_z(self._grid_origin)

        # -- Проверка возможности переиспользования сетки ------------------
        can_reuse = (
            self._grid_ready
            and self._grid_cached_cell_size == int(cell_size)
            and self._grid_cached_z_size == z_size_int
            and self._grid_cells
            and self._grid_nodes
        )

        if can_reuse:
            # Сбрасываем только состояние выделения, сетку показываем заново
            self._zone_selection_mode = True
            self._current_zone_size = cell_size
            self._current_zone_z_size = z_size_int
            self._selected_cells = set()
            self._selected_height = 0
            self._selection_anchor = None
            self._selection_start_cell = None
            self._volume_locked_from_contour = False
            self._contour_points = []
            self._final_contour_points = []
            self._grid_surface_meshes = getattr(self, "_grid_surface_meshes", [])
            self._contour_points_actor = None
            self._contour_lines_actor = None
            self._last_volume_start_height = grid_z
            self._show_grid_actors()
            if progress_callback:
                try:
                    progress_callback(100)
                except Exception as e:
                    log_exception(__name__, "start_zone_selection_grid.progress_callback", e)
            return

        # -- Полное построение сетки (первый раз или параметры изменились) --
        self._zone_selection_mode = True
        self._current_zone_size = cell_size
        self._current_zone_z_size = z_size_int
        self._selected_cells = set()
        self._selected_height = 0
        self._selection_anchor = None
        self._selection_start_cell = None
        self._volume_locked_from_contour = False
        self._contour_points = []
        self._final_contour_points = []
        self._grid_nodes = []
        self._grid_surface_meshes = []
        self._contour_points_actor = None
        self._contour_lines_actor = None
        self._last_volume_start_height = grid_z

        built = self.create_grid_on_floor(
            min_x, max_x, min_y, max_y, grid_z, cell_size,
            grid_origin=self._grid_origin,
            progress_callback=progress_callback,
        )
        if not built:
            self._zone_selection_mode = False
            self._grid_ready = False
            self._grid_cached_cell_size = 0
            self._grid_cached_z_size = 0
            return

        # Обновляем кэш
        self._grid_ready = True
        self._grid_cached_cell_size = int(cell_size)
        self._grid_cached_z_size = z_size_int

    def ensure_visible_grid(
        self: "ModelViewWidget",
        cell_size: int,
        grid_origin: Optional[Tuple[float, float, float]] = None,
        z_size: Optional[int] = None,
        progress_callback: Optional[Callable[[int], None]] = None,
        cache_path: Optional[str] = None,
        cache_extra: Optional[dict] = None,
    ) -> bool:
        """Обеспечить постоянное отображение сетки без включения режима разметки."""
        if not self._models_loaded or not self._plotter:
            return False
        cell_size_int = int(cell_size)
        z_size_int = int(z_size) if z_size is not None else int(cell_size_int)
        cache_meta = self._build_grid_cache_meta(
            cell_size=cell_size_int,
            z_size=z_size_int,
            grid_origin=grid_origin,
            extra=cache_extra,
        )
        try:
            loaded_from_cache = False
            if cache_path:
                loaded_from_cache = self.load_grid_cache(str(cache_path), expected_meta=cache_meta)
            if not loaded_from_cache:
                self.start_zone_selection_grid(
                    cell_size_int,
                    grid_origin=grid_origin,
                    progress_callback=progress_callback,
                    z_size=z_size_int,
                )
                if cache_path and self.has_grid():
                    self.save_grid_cache(str(cache_path), meta=cache_meta)
            else:
                self._show_grid_actors()

            if not self.has_grid():
                return False
            if self._zone_selection_mode:
                self.cancel_zone_selection()
            self._show_grid_actors()
            return True
        except Exception as e:
            log_exception(__name__, "ensure_visible_grid.main", e)
            return False

    def _mesh_signature(self: "ModelViewWidget", mesh) -> dict:
        if mesh is None:
            return {"present": False}
        try:
            bounds = tuple(float(v) for v in mesh.bounds)
        except Exception as e:
            log_exception(__name__, "_mesh_signature.bounds", e)
            bounds = ()
        try:
            n_points = int(getattr(mesh, "n_points", 0) or 0)
        except Exception as e:
            log_exception(__name__, "_mesh_signature.n_points", e)
            n_points = 0
        try:
            n_cells = int(getattr(mesh, "n_cells", 0) or 0)
        except Exception as e:
            log_exception(__name__, "_mesh_signature.n_cells", e)
            n_cells = 0
        raw = f"{bounds}|{n_points}|{n_cells}".encode("utf-8", errors="ignore")
        digest = hashlib.sha1(raw).hexdigest()[:16]
        return {
            "present": True,
            "bounds": [round(float(v), 4) for v in bounds],
            "n_points": n_points,
            "n_cells": n_cells,
            "sig": digest,
        }

    def _build_grid_cache_meta(
        self: "ModelViewWidget",
        *,
        cell_size: int,
        z_size: int,
        grid_origin: Optional[Tuple[float, float, float]],
        extra: Optional[dict] = None,
    ) -> dict:
        origin = tuple(grid_origin or getattr(self, "_grid_origin", (0.0, 0.0, 0.0)) or (0.0, 0.0, 0.0))
        room_bounds = tuple(getattr(self, "_room_bounds", ()) or ())
        meta = {
            "version": 1,
            "cell_size": int(cell_size),
            "z_size": int(z_size),
            "grid_origin": [round(float(origin[0]), 4), round(float(origin[1]), 4), round(float(origin[2]), 4)],
            "room_bounds": [round(float(v), 4) for v in room_bounds] if room_bounds else [],
            "floor": self._mesh_signature(getattr(self, "_floor_mesh", None)),
            "walls": self._mesh_signature(getattr(self, "_walls_mesh", None)),
        }
        if extra:
            meta["extra"] = dict(extra)
        return meta

    def make_grid_cache_meta(
        self: "ModelViewWidget",
        *,
        cell_size: int,
        z_size: int,
        grid_origin: Optional[Tuple[float, float, float]] = None,
        extra: Optional[dict] = None,
    ) -> dict:
        """Публичный конструктор метаданных кэша сетки."""
        return self._build_grid_cache_meta(
            cell_size=int(cell_size),
            z_size=int(z_size),
            grid_origin=grid_origin,
            extra=extra,
        )

    def _grid_cache_payload(
        self: "ModelViewWidget",
        meta: dict,
    ) -> dict:
        cells = []
        for cell_id, bounds in (getattr(self, "_grid_cells", {}) or {}).items():
            try:
                mn_x, mx_x, mn_y, mx_y = bounds
                cells.append([
                    int(cell_id),
                    float(mn_x),
                    float(mx_x),
                    float(mn_y),
                    float(mx_y),
                ])
            except Exception as e:
                log_exception(__name__, "_grid_cache_payload.cell_convert", e)
                continue
        cells.sort(key=lambda item: item[0])
        return {"meta": meta, "cells": cells}

    def save_grid_cache(self: "ModelViewWidget", cache_path: str, meta: dict) -> bool:
        """Сохранить текущую сетку в файл кэша."""
        if not self.has_grid():
            return False
        try:
            path = Path(str(cache_path))
            path.parent.mkdir(parents=True, exist_ok=True)
            payload = self._grid_cache_payload(meta)
            path.write_text(json.dumps(payload, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
            return True
        except Exception as e:
            log_exception(__name__, "save_grid_cache.write", e)
            return False

    def _render_grid_actor_from_cells(
        self: "ModelViewWidget",
        *,
        min_z: float,
    ) -> bool:
        if pv is None or not self._plotter:
            return False
        try:
            for mesh in list(getattr(self, "_grid_meshes", [])):
                try:
                    self._plotter.remove_actor(mesh)
                except Exception as e:
                    log_exception(__name__, "_render_grid_actor_from_cells.remove_actor", e)
            self._grid_meshes = []
            line_points: list[list[float]] = []
            line_cells: list[int] = []

            def add_segment(p1: tuple[float, float, float], p2: tuple[float, float, float]) -> None:
                i1 = len(line_points)
                line_points.append([float(p1[0]), float(p1[1]), float(p1[2])])
                i2 = len(line_points)
                line_points.append([float(p2[0]), float(p2[1]), float(p2[2])])
                line_cells.extend([2, i1, i2])

            for mn_x, mx_x, mn_y, mx_y in (getattr(self, "_grid_cells", {}) or {}).values():
                p00 = (mn_x, mn_y, min_z)
                p10 = (mx_x, mn_y, min_z)
                p11 = (mx_x, mx_y, min_z)
                p01 = (mn_x, mx_y, min_z)
                add_segment(p00, p10)
                add_segment(p10, p11)
                add_segment(p11, p01)
                add_segment(p01, p00)
            if not line_points or not line_cells:
                return False
            grid_lines = pv.PolyData(line_points)
            grid_lines.lines = line_cells
            actor = self._plotter.add_mesh(grid_lines, color=self._get_grid_color(), line_width=1)
            self._grid_meshes.append(actor)
            self._plotter.update()
            return True
        except Exception as e:
            log_exception(__name__, "_render_grid_actor_from_cells.main", e)
            return False

    def load_grid_cache(self: "ModelViewWidget", cache_path: str, expected_meta: dict) -> bool:
        """Загрузить сетку из файла кэша при полном совпадении метаданных."""
        try:
            path = Path(str(cache_path))
            if not path.exists():
                return False
            raw = path.read_text(encoding="utf-8")
            payload = json.loads(raw)
            cached_meta = dict(payload.get("meta") or {})
            if cached_meta != dict(expected_meta or {}):
                return False
            cells_raw = list(payload.get("cells") or [])
            if not cells_raw:
                return False
            grid_cells: dict[int, tuple[float, float, float, float]] = {}
            for item in cells_raw:
                if not isinstance(item, list) or len(item) != 5:
                    continue
                cell_id = int(item[0])
                mn_x = float(item[1])
                mx_x = float(item[2])
                mn_y = float(item[3])
                mx_y = float(item[4])
                grid_cells[cell_id] = (mn_x, mx_x, mn_y, mx_y)
            if not grid_cells:
                return False

            for mesh in list(getattr(self, "_grid_meshes", [])):
                try:
                    self._plotter.remove_actor(mesh)
                except Exception as e:
                    log_exception(__name__, "load_grid_cache.remove_actor", e)
            self._grid_meshes = []
            self._grid_cells = grid_cells
            self._rebuild_grid_nodes()
            origin = expected_meta.get("grid_origin") or [0.0, 0.0, 0.0]
            self._grid_origin = (float(origin[0]), float(origin[1]), float(origin[2]))
            self._current_zone_size = int(expected_meta.get("cell_size", 0) or 0)
            self._current_zone_z_size = int(expected_meta.get("z_size", self._current_zone_size) or self._current_zone_size)
            self._grid_cached_cell_size = int(self._current_zone_size)
            self._grid_cached_z_size = int(self._current_zone_z_size)
            self._grid_ready = True
            self._zone_selection_mode = False
            grid_z = self._get_grid_plane_z(self._grid_origin)
            if not self._render_grid_actor_from_cells(min_z=grid_z):
                self.clear_grid()
                return False
            return True
        except Exception as e:
            log_exception(__name__, "load_grid_cache.main", e)
            return False

    def create_grid_on_floor(
        self: "ModelViewWidget",
        min_x: float, max_x: float,
        min_y: float, max_y: float,
        min_z: float,
        cell_size: int,
        grid_origin: Optional[Tuple[float, float, float]] = None,
        progress_callback: Optional[Callable[[int], None]] = None,
    ) -> bool:
        """Построить линии сетки на полу."""
        for mesh in self._grid_meshes:
            try:
                self._plotter.remove_actor(mesh)
            except Exception as e:
                log_exception(__name__, "create_grid_on_floor.remove_actor", e)
        self._grid_meshes = []
        self._grid_cells = {}

        disp_min_x = float(min_x)
        disp_max_x = float(max_x)
        disp_min_y = float(min_y)
        disp_max_y = float(max_y)

        # Выровнять шаг сетки по локальной опорной точке объекта.
        if grid_origin is not None and cell_size > 0:
            ox, oy, _ = grid_origin
            disp_min_x = ox + math.floor((disp_min_x - ox) / cell_size) * cell_size
            disp_max_x = ox + math.ceil((disp_max_x - ox) / cell_size) * cell_size
            disp_min_y = oy + math.floor((disp_min_y - oy) / cell_size) * cell_size
            disp_max_y = oy + math.ceil((disp_max_y - oy) / cell_size) * cell_size

        floor_triangles = self._extract_floor_triangles_2d()

        x = disp_min_x
        cell_id = 0
        line_points: list[list[float]] = []
        line_cells: list[int] = []
        total_cols = max(1, int(math.ceil(
            (disp_max_x - disp_min_x) / max(1.0, float(cell_size))
        )))
        col_idx = 0

        def add_segment(p1: tuple[float, float, float], p2: tuple[float, float, float]) -> None:
            i1 = len(line_points)
            line_points.append([p1[0], p1[1], p1[2]])
            i2 = len(line_points)
            line_points.append([p2[0], p2[1], p2[2]])
            line_cells.extend([2, i1, i2])

        while x < disp_max_x:
            if bool(getattr(self, "_grid_build_cancel_requested", False)):
                self._grid_cells = {}
                self._grid_nodes = []
                return False
            y = disp_min_y
            while y < disp_max_y:
                if bool(getattr(self, "_grid_build_cancel_requested", False)):
                    self._grid_cells = {}
                    self._grid_nodes = []
                    return False
                x_next = min(x + cell_size, disp_max_x)
                y_next = min(y + cell_size, disp_max_y)
                if self._is_cell_inside_floor_2d(x, x_next, y, y_next, floor_triangles):
                    self._grid_cells[cell_id] = (x, x_next, y, y_next)
                    p00 = (x, y, min_z)
                    p10 = (x_next, y, min_z)
                    p11 = (x_next, y_next, min_z)
                    p01 = (x, y_next, min_z)
                    add_segment(p00, p10)
                    add_segment(p10, p11)
                    add_segment(p11, p01)
                    add_segment(p01, p00)
                    cell_id += 1
                y = y_next
            x = x_next
            col_idx += 1
            if progress_callback:
                try:
                    progress_callback(min(100, int((col_idx / total_cols) * 100)))
                except Exception as e:
                    log_exception(__name__, "create_grid_on_floor.progress_callback", e)

        if line_points and line_cells:
            try:
                grid_lines = pv.PolyData(line_points)
                grid_lines.lines = line_cells
                actor = self._plotter.add_mesh(
                    grid_lines,
                    color=self._get_grid_color(),
                    line_width=1,
                )
                self._grid_meshes.append(actor)
            except Exception as e:
                log_exception(__name__, "create_grid_on_floor.add_mesh", e)

        self._rebuild_grid_nodes()
        if progress_callback:
            try:
                progress_callback(100)
            except Exception as e:
                log_exception(__name__, "create_grid_on_floor.progress_complete", e)
        self._plotter.update()
        return True

    def has_grid(self: "ModelViewWidget") -> bool:
        """Проверить наличие готовой разметочной сетки."""
        return bool(
            getattr(self, "_grid_ready", False)
            and getattr(self, "_grid_cells", None)
            and getattr(self, "_grid_nodes", None)
        )

    def clear_grid(self: "ModelViewWidget") -> None:
        """Полностью уничтожить сетку (акторы + данные + cache)."""
        for mesh in list(getattr(self, "_grid_meshes", [])):
            try:
                self._plotter.remove_actor(mesh)
            except Exception as e:
                log_exception(__name__, "clear_grid.remove_actor", e)
        self._grid_meshes = []
        self._grid_cells = {}
        self._grid_nodes = []
        self._grid_ready = False
        self._grid_cached_cell_size = 0
        self._grid_cached_z_size = 0

    def _show_grid_actors(self: "ModelViewWidget") -> None:
        """Показать скрытые акторы разметочной сетки."""
        for actor in getattr(self, "_grid_meshes", []):
            try:
                actor.VisibilityOn()
            except Exception as e:
                log_exception(__name__, "_show_grid_actors.visibility_on", e)
        try:
            if self._plotter:
                self._plotter.update()
        except Exception as e:
            log_exception(__name__, "_show_grid_actors.plotter_update", e)

    def _hide_grid_actors(self: "ModelViewWidget") -> None:
        """Скрыть акторы разметочной сетки без удаления."""
        for actor in getattr(self, "_grid_meshes", []):
            try:
                actor.VisibilityOff()
            except Exception as e:
                log_exception(__name__, "_hide_grid_actors.visibility_off", e)
        try:
            if self._plotter:
                self._plotter.update()
        except Exception as e:
            log_exception(__name__, "_hide_grid_actors.plotter_update", e)

    def _rebuild_grid_nodes(self: "ModelViewWidget") -> None:
        nodes = set()
        for mn_x, mx_x, mn_y, mx_y in self._grid_cells.values():
            nodes.add((float(mn_x), float(mn_y)))
            nodes.add((float(mx_x), float(mn_y)))
            nodes.add((float(mx_x), float(mx_y)))
            nodes.add((float(mn_x), float(mx_y)))
        self._grid_nodes = list(nodes)

    def _nearest_grid_node(
        self: "ModelViewWidget", x: float, y: float,
    ) -> Optional[Tuple[float, float]]:
        use_surface = bool(
            getattr(self, "_contour_zone_overlay_enabled", False)
            and getattr(self, "_contour_zone_mode", None) == "overlay"
            and getattr(self, "_grid_surface_nodes", None)
        )
        nodes = self._grid_surface_nodes if use_surface else self._grid_nodes
        if not nodes:
            return None
        step = max(1.0, float(getattr(self, "_current_zone_size", 1.0)))
        max_dist2 = (step * 0.55) ** 2
        best = None
        best_dist2 = None
        for nx, ny in nodes:
            d2 = (nx - x) ** 2 + (ny - y) ** 2
            if best_dist2 is None or d2 < best_dist2:
                best_dist2 = d2
                best = (nx, ny)
        if best is None or best_dist2 is None or best_dist2 > max_dist2:
            return None
        return best

    def _get_grid_plane_z(
        self: "ModelViewWidget",
        grid_origin: Optional[Tuple[float, float, float]] = None,
    ) -> float:
        """Определить Z-плоскость построения сетки."""
        if grid_origin is not None:
            try:
                return float(grid_origin[2])
            except Exception as e:
                log_exception(__name__, "_get_grid_plane_z.origin_z", e)
        if self._floor_mesh is not None:
            return float(self._floor_mesh.bounds[5])
        if self._room_bounds:
            return float(self._room_bounds[5])
        return 0.0


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Базовые операции сетки: построение, отображение, скрытие и привязка к узлам.
#
# 2) Последовательность действий и вызовов:
#    A. Класс GridCoreBaseMixin: точки входа
#       Публичные методы сценария:
#       - GridCoreBaseMixin.start_zone_selection_grid(...)
#       - GridCoreBaseMixin.create_grid_on_floor(...)
#       - GridCoreBaseMixin.has_grid(...)
#       - GridCoreBaseMixin.clear_grid(...)
#
#    B. GridCoreBaseMixin: запуск и настройка:
#       GridCoreBaseMixin.start_zone_selection_grid(...)
#         Назначение: Включить режим выбора зоны через сетку на полу.
#         Последовательность внутренних вызовов:
#         -> GridCoreBaseMixin._get_grid_plane_z(...)
#         -> GridCoreBaseMixin.create_grid_on_floor(...)
#         -> GridCoreBaseMixin._show_grid_actors(...)
#
#    C. GridCoreBaseMixin: основной сценарий:
#       GridCoreBaseMixin._apply_grid_theme(...)
#         Назначение: Перекрасить акторы сетки в соответствии с текущей темой.
#         Последовательность внутренних вызовов:
#         -> GridCoreBaseMixin._get_grid_color(...)
#       GridCoreBaseMixin.create_grid_on_floor(...)
#         Назначение: Построить линии сетки на полу.
#         Последовательность внутренних вызовов:
#         -> GridCoreBaseMixin._rebuild_grid_nodes(...)
#         -> GridCoreBaseMixin._get_grid_color(...)
#       GridCoreBaseMixin.has_grid(...)
#         Назначение: Проверить наличие готовой разметочной сетки.
#       GridCoreBaseMixin._show_grid_actors(...)
#         Назначение: Показать скрытые акторы разметочной сетки.
#       GridCoreBaseMixin._rebuild_grid_nodes(...)
#         Назначение: перестраивает grid nodes в рамках текущего сценария модуля.
#
#    D. GridCoreBaseMixin: завершение и очистка:
#       GridCoreBaseMixin.clear_grid(...)
#         Назначение: Полностью уничтожить сетку (акторы + данные + cache).
#
#    E. GridCoreBaseMixin: вспомогательные расчёты:
#       GridCoreBaseMixin._get_grid_color(...)
#         Назначение: возвращает grid color в рамках текущего сценария модуля.
#       GridCoreBaseMixin._hide_grid_actors(...)
#         Назначение: Скрыть акторы разметочной сетки без удаления.
#       GridCoreBaseMixin._nearest_grid_node(...)
#         Назначение: выполняет шаг "nearest grid node" в рамках текущего сценария модуля.
#       GridCoreBaseMixin._get_grid_plane_z(...)
#         Назначение: Определить Z-плоскость построения сетки.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Геометрическая визуализация зависит от pyvista/vtk; при недоступности модуль обязан завершать шаг без падения сценария.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
