# -*- coding: utf-8 -*-
# gui/components/model_view/_mv_zones_crud.py
# CRUD зон + пространственные запросы

from __future__ import annotations

from typing import Optional, Tuple, TYPE_CHECKING

from PySide6.QtGui import QColor
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
    _PV = True
except ImportError:
    _PV = False


class ZoneCrudMixin:
    """CRUD-операции с зонами и пространственные запросы."""

    def add_zone(
        self: "ModelViewWidget",
        zone_id: str,
        volume_points: list[tuple[float, float]],
        zone_start_height: float,
        zone_height: float,
        color: str,
    ):
        """Добавить зону по контуру (XY) и высоте."""
        if not self._models_loaded or not self._plotter:
            return False

        if not volume_points or len(volume_points) < 3:
            return False

        # Безопасность: если зона уже существует, сначала удаляем, чтобы избежать утечки акторов.
        if zone_id in self._zones:
            self.remove_zone(zone_id)

        try:
            qcolor = QColor(color)
            rgb_color = (qcolor.redF(), qcolor.greenF(), qcolor.blueF())
            opacity = qcolor.alphaF()

            mesh = self._build_zone_mesh(volume_points, zone_start_height, zone_height)
            if mesh is None:
                return False

            actor = self._plotter.add_mesh(
                mesh, color=rgb_color, opacity=opacity,
                show_edges=False, line_width=2,
            )

            min_x, max_x, min_y, max_y, min_z, max_z = self._compute_zone_bounds(
                volume_points, zone_start_height, zone_height
            )
            self._zones[zone_id] = actor
            self._zone_data[zone_id] = (min_x, max_x, min_y, max_y, min_z, max_z)
            self._zone_polygons[zone_id] = [(float(x), float(y)) for x, y in volume_points]
            self._zone_heights[zone_id] = (float(zone_start_height), float(zone_height))
            self._plotter.update()
            return True

        except Exception as e:
            print(f"Zone add error: {e}")
            return False

    def update_zone(
        self: "ModelViewWidget",
        zone_id: str,
        volume_points: list[tuple[float, float]],
        zone_start_height: float,
        zone_height: float,
        color: str,
    ):
        """Обновить зону."""
        self.remove_zone(zone_id)
        return self.add_zone(
            zone_id,
            volume_points,
            zone_start_height,
            zone_height,
            color,
        )

    def remove_zone(self: "ModelViewWidget", zone_id: str):
        """Удалить зону."""
        if getattr(self, "_selected_zone_highlight_id", None) == zone_id:
            self.clear_selected_zone_highlight()
        if getattr(self, "_hover_highlighted_zone_id", None) == zone_id:
            self._unhighlight_hover_zone()
        if zone_id in self._zones:
            actor = self._zones.pop(zone_id)
            if actor is not None and self._plotter is not None:
                try:
                    self._plotter.remove_actor(actor)
                except Exception as e:
                    log_exception(__name__, "remove_zone", e)
        if zone_id in self._zone_data:
            del self._zone_data[zone_id]
        if zone_id in self._zone_polygons:
            del self._zone_polygons[zone_id]
        if zone_id in self._zone_heights:
            del self._zone_heights[zone_id]
        self._remove_zone_facility_contour(zone_id)

    def _build_zone_mesh(
        self: "ModelViewWidget",
        volume_points: list[tuple[float, float]],
        zone_start_height: float,
        zone_height: float,
    ):
        if not _PV or not volume_points or len(volume_points) < 3:
            return None
        pts = [(float(x), float(y), float(zone_start_height)) for x, y in volume_points]
        faces = [len(pts)] + list(range(len(pts)))
        poly = pv.PolyData(pts, faces)
        try:
            poly = poly.triangulate()
        except Exception as _exc:
            log_exception(__name__, "_build_zone_mesh", _exc)
        return poly.extrude([0.0, 0.0, float(zone_height)], capping=True)

    def _compute_zone_bounds(
        self: "ModelViewWidget",
        volume_points: list[tuple[float, float]],
        zone_start_height: float,
        zone_height: float,
    ) -> Tuple[float, float, float, float, float, float]:
        min_x = min(p[0] for p in volume_points)
        max_x = max(p[0] for p in volume_points)
        min_y = min(p[1] for p in volume_points)
        max_y = max(p[1] for p in volume_points)
        min_z = float(zone_start_height)
        max_z = float(zone_start_height) + float(zone_height)
        return min_x, max_x, min_y, max_y, min_z, max_z

    def _find_zone_at_point(
        self: "ModelViewWidget", x: float, y: float, z: float,
    ) -> Optional[str]:
        """Найти зону по точке в мировых координатах."""
        tolerance = 1.0
        for zone_id, (mn_x, mx_x, mn_y, mx_y, mn_z, mx_z) in self._zone_data.items():
            if (
                (mn_x - tolerance) <= x <= (mx_x + tolerance)
                and (mn_y - tolerance) <= y <= (mx_y + tolerance)
                and (mn_z - tolerance) <= z <= (mx_z + tolerance)
            ):
                return zone_id
        return None

    def _point_on_segment_2d(
        self: "ModelViewWidget",
        px: float,
        py: float,
        a: tuple[float, float],
        b: tuple[float, float],
        eps: float = 1e-6,
    ) -> bool:
        ax, ay = a
        bx, by = b
        cross = (px - ax) * (by - ay) - (py - ay) * (bx - ax)
        if abs(cross) > eps:
            return False
        dot = (px - ax) * (bx - ax) + (py - ay) * (by - ay)
        if dot < -eps:
            return False
        sq_len = (bx - ax) * (bx - ax) + (by - ay) * (by - ay)
        if dot - sq_len > eps:
            return False
        return True

    def _classify_point_in_polygon(
        self: "ModelViewWidget",
        px: float,
        py: float,
        polygon: list[tuple[float, float]],
        eps: float = 1e-6,
    ) -> tuple[bool, bool]:
        """Возвращает (внутри, граница)."""
        if not polygon or len(polygon) < 3:
            return False, False
        inside = False
        n = len(polygon)
        for i in range(n):
            a = polygon[i]
            b = polygon[(i + 1) % n]
            if self._point_on_segment_2d(px, py, a, b, eps):
                return True, True
            ax, ay = a
            bx, by = b
            intersects = ((ay > py) != (by > py)) and (
                px < (bx - ax) * (py - ay) / (by - ay + eps) + ax
            )
            if intersects:
                inside = not inside
        return inside, False

    def _zone_intersects_height(
        self: "ModelViewWidget",
        zone_id: str,
        plane_z: float,
        eps: float = 1e-6,
    ) -> bool:
        start_height, height = self._zone_heights.get(zone_id, (0.0, 0.0))
        zone_min = float(start_height)
        zone_max = float(start_height) + float(height)
        z = float(plane_z)
        return (zone_min - eps) <= z <= (zone_max + eps)

    def _classify_point_in_zones(
        self: "ModelViewWidget",
        px: float,
        py: float,
        plane_z: Optional[float] = None,
    ) -> tuple[Optional[str], Optional[str]]:
        """Возвращает (zone_id, kind) для верхней зоны в точке."""
        ranked = self._classify_point_in_zones_ranked(px, py, plane_z=plane_z)
        if not ranked:
            return None, None
        zone_id, kind, _ = ranked[0]
        return zone_id, kind

    def _classify_point_in_zones_ranked(
        self: "ModelViewWidget",
        px: float,
        py: float,
        plane_z: Optional[float] = None,
    ) -> list[tuple[str, str, float]]:
        """Возвращает все совпадающие зоны, отсортированные сверху вниз."""
        step = max(1.0, float(getattr(self, "_current_zone_size", 1.0)))
        eps = max(1e-6, step * 0.25)
        ignore_zone_id = getattr(self, "_contour_ignore_zone_id", None)
        candidates: list[tuple[float, int, str, str]] = []
        for zone_id, polygon in self._zone_polygons.items():
            if ignore_zone_id and zone_id == ignore_zone_id:
                continue
            if plane_z is not None and not self._zone_intersects_height(zone_id, plane_z, eps=eps):
                continue
            inside, boundary = self._classify_point_in_polygon(px, py, polygon, eps=eps)
            if not inside:
                continue
            top_z = self._get_zone_top_height(zone_id)
            rank = 0 if boundary else 1
            kind = "boundary" if boundary else "inside"
            candidates.append((float(top_z), rank, zone_id, kind))

        if not candidates:
            return []

        candidates.sort(key=lambda item: (item[0], item[1]), reverse=True)
        return [(zone_id, kind, top_z) for top_z, _, zone_id, kind in candidates]

    def _point_inside_any_zone(
        self: "ModelViewWidget",
        px: float,
        py: float,
        plane_z: Optional[float] = None,
    ) -> bool:
        step = max(1.0, float(getattr(self, "_current_zone_size", 1.0)))
        eps = max(1e-6, step * 0.25)
        for zone_id, polygon in self._zone_polygons.items():
            if plane_z is not None and not self._zone_intersects_height(zone_id, plane_z, eps=eps):
                continue
            inside, boundary = self._classify_point_in_polygon(px, py, polygon, eps=eps)
            if inside and not boundary:
                return True
        return False

    def _point_in_zone_or_boundary(
        self: "ModelViewWidget",
        px: float,
        py: float,
        zone_id: str,
        plane_z: Optional[float] = None,
    ) -> bool:
        polygon = self._zone_polygons.get(zone_id)
        if not polygon:
            return False
        step = max(1.0, float(getattr(self, "_current_zone_size", 1.0)))
        eps = max(1e-6, step * 0.25)
        if plane_z is not None and not self._zone_intersects_height(zone_id, plane_z, eps=eps):
            return False
        inside, boundary = self._classify_point_in_polygon(px, py, polygon, eps=eps)
        return inside or boundary

    def _get_zone_top_height(self: "ModelViewWidget", zone_id: str) -> float:
        start_height, height = self._zone_heights.get(zone_id, (0.0, 0.0))
        return float(start_height) + float(height)


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Создание, обновление, удаление зон и геометрические проверки принадлежности.
#
# 2) Последовательность действий и вызовов:
#    A. Класс ZoneCrudMixin: точки входа
#       Публичные методы сценария:
#       - ZoneCrudMixin.add_zone(...)
#       - ZoneCrudMixin.update_zone(...)
#       - ZoneCrudMixin.remove_zone(...)
#
#    B. ZoneCrudMixin: основной сценарий:
#       ZoneCrudMixin.add_zone(...)
#         Назначение: Добавить зону по контуру (XY) и высоте.
#         Последовательность внутренних вызовов:
#         -> ZoneCrudMixin.remove_zone(...)
#         -> ZoneCrudMixin._build_zone_mesh(...)
#         -> ZoneCrudMixin._compute_zone_bounds(...)
#       ZoneCrudMixin.update_zone(...)
#         Назначение: Обновить зону.
#         Последовательность внутренних вызовов:
#         -> ZoneCrudMixin.remove_zone(...)
#         -> ZoneCrudMixin.add_zone(...)
#       ZoneCrudMixin._compute_zone_bounds(...)
#         Назначение: вычисляет zone bounds в рамках текущего сценария модуля.
#       ZoneCrudMixin._find_zone_at_point(...)
#         Назначение: Найти зону по точке в мировых координатах.
#       ZoneCrudMixin._classify_point_in_polygon(...)
#         Назначение: Возвращает (внутри, граница).
#         Последовательность внутренних вызовов:
#         -> ZoneCrudMixin._point_on_segment_2d(...)
#       ZoneCrudMixin._classify_point_in_zones(...)
#         Назначение: Возвращает (zone_id, kind) для верхней зоны в точке.
#         Последовательность внутренних вызовов:
#         -> ZoneCrudMixin._classify_point_in_zones_ranked(...)
#       ZoneCrudMixin._classify_point_in_zones_ranked(...)
#         Назначение: Возвращает все совпадающие зоны, отсортированные сверху вниз.
#         Последовательность внутренних вызовов:
#         -> ZoneCrudMixin._classify_point_in_polygon(...)
#         -> ZoneCrudMixin._get_zone_top_height(...)
#         -> ZoneCrudMixin._zone_intersects_height(...)
#
#    C. ZoneCrudMixin: завершение и очистка:
#       ZoneCrudMixin.remove_zone(...)
#         Назначение: Удалить зону.
#
#    D. ZoneCrudMixin: вспомогательные расчёты:
#       ZoneCrudMixin._build_zone_mesh(...)
#         Назначение: строит zone mesh в рамках текущего сценария модуля.
#       ZoneCrudMixin._point_on_segment_2d(...)
#         Назначение: выполняет шаг "point on segment 2d" в рамках текущего сценария модуля.
#       ZoneCrudMixin._zone_intersects_height(...)
#         Назначение: выполняет шаг "zone intersects height" в рамках текущего сценария модуля.
#       ZoneCrudMixin._point_inside_any_zone(...)
#         Назначение: выполняет шаг "point inside any zone" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> ZoneCrudMixin._classify_point_in_polygon(...)
#         -> ZoneCrudMixin._zone_intersects_height(...)
#       ZoneCrudMixin._point_in_zone_or_boundary(...)
#         Назначение: выполняет шаг "point in zone or boundary" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> ZoneCrudMixin._classify_point_in_polygon(...)
#         -> ZoneCrudMixin._zone_intersects_height(...)
#       ZoneCrudMixin._get_zone_top_height(...)
#         Назначение: возвращает zone top height в рамках текущего сценария модуля.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Геометрическая визуализация зависит от pyvista/vtk; при недоступности модуль обязан завершать шаг без падения сценария.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
