# -*- coding: utf-8 -*-
"""Миксин обнаружения столкновений и валидации стеллажей."""
from __future__ import annotations

from typing import Any, TYPE_CHECKING

from gui.components.model_view._mv_rack_geometry import (
    MIN_AISLE_MM, MIN_WALL_BUFFER_MM, bboxes_intersect,
    rack_bbox, rack_aisle_bboxes,
)
from error_logger import log_exception

if TYPE_CHECKING:
    from gui.components.model_view_widget import ModelViewWidget


class RackCollisionMixin:
    """Вспомогательные методы проверки столкновений / валидации стеллажей, подмешиваемые в *ModelViewWidget*."""

    # -- высота / модель -----------------------------------------------------

    @staticmethod
    def _rack_height_mm(params: dict[str, Any]) -> float:
        try:
            return max(0.0, float(params.get("footprint_height_mm", 1800)))
        except Exception as exc:
            log_exception(__name__, "_rack_height_mm", exc)
            return 1800.0

    @staticmethod
    def _has_additional_model(params: dict[str, Any]) -> bool:
        additional_model = dict(params.get("additional_model") or {})
        if additional_model.get("model_path"):
            return True
        # Обратная совместимость: старые данные могут хранить путь на верхнем уровне.
        return bool(params.get("model_path") or params.get("custom_model_path"))

    def _is_imported_model_entry(self: "ModelViewWidget", entry: dict[str, Any]) -> bool:
        params = dict((entry or {}).get("params") or {})
        if self._has_additional_model(params):
            return True
        rack_type = str(params.get("rack_type") or "").strip().lower()
        return rack_type in {"mezo", "mezz", "mezzanine", "custom", "additional_model"}

    def _force_show_imported_model_entry(self: "ModelViewWidget", entry: dict[str, Any]) -> bool:
        return bool(self._imported_models_enabled) and self._is_imported_model_entry(entry)

    def set_imported_models_enabled(self: "ModelViewWidget", enabled: bool) -> None:
        """Устанавливает глобальную политику видимости для импортированных пользовательских моделей."""
        self._imported_models_enabled = bool(enabled)
        for entry in self._rack_entries:
            if not self._is_imported_model_entry(entry):
                continue
            visible = bool(self._imported_models_enabled)
            for actor in entry.get("actors", []):
                try:
                    actor.SetVisibility(1 if visible else 0)
                except Exception as _exc:
                    log_exception(__name__, "set_imported_models_enabled", _exc)
            self._set_rack_shelf_actors_visibility(str(entry.get("rack_id") or ""), visible)
            self._apply_rack_helper_visibility_policy(entry, visible)
        if self._plotter:
            self._plotter.update()

    @staticmethod
    def _is_pallet_params(params: dict[str, Any]) -> bool:
        return str(dict(params or {}).get("rack_type") or "").strip().lower() == "pallet"

    # -- bbox / буферы -------------------------------------------------------

    @classmethod
    def _rack_access_aisle_mm(cls, params: dict[str, Any]) -> float:
        if cls._is_pallet_params(params):
            return 2300.0
        return float(MIN_AISLE_MM)

    @classmethod
    def _rack_wall_buffer_mm(cls, params: dict[str, Any]) -> float:
        if cls._is_pallet_params(params):
            return float(getattr(cls, "_PALLET_BUFFER_MM", MIN_WALL_BUFFER_MM))
        return 0.0

    @classmethod
    def _rack_placement_bbox(
        cls,
        cx: float,
        cy: float,
        params: dict[str, Any],
        rotation: int,
    ) -> tuple[float, float, float, float]:
        return rack_bbox(
            cx,
            cy,
            params,
            rotation,
            wall_buffer=cls._rack_wall_buffer_mm(params),
        )

    @classmethod
    def _rack_collision_buffer_mm(cls, params: dict[str, Any]) -> float:
        # Буфер столкновений единый для проверок стеллаж-стеллаж.
        # Для PALLET не менее его настроенного буфера.
        base = float(getattr(cls, "_RACK_COLLISION_BUFFER_MM", MIN_WALL_BUFFER_MM))
        if cls._is_pallet_params(params):
            return max(base, float(getattr(cls, "_PALLET_BUFFER_MM", MIN_WALL_BUFFER_MM)))
        return base

    @classmethod
    def _rack_collision_bbox(
        cls,
        cx: float,
        cy: float,
        params: dict[str, Any],
        rotation: int,
    ) -> tuple[float, float, float, float]:
        return rack_bbox(
            cx,
            cy,
            params,
            rotation,
            wall_buffer=cls._rack_collision_buffer_mm(params),
        )

    @staticmethod
    def _bbox_overlap_xy_mm(
        a: tuple[float, float, float, float],
        b: tuple[float, float, float, float],
    ) -> tuple[float, float]:
        a_min_x, a_max_x, a_min_y, a_max_y = a
        b_min_x, b_max_x, b_min_y, b_max_y = b
        overlap_x = min(a_max_x, b_max_x) - max(a_min_x, b_min_x)
        overlap_y = min(a_max_y, b_max_y) - max(a_min_y, b_min_y)
        return float(overlap_x), float(overlap_y)

    @staticmethod
    def _bbox_contains(
        outer: tuple[float, float, float, float],
        inner: tuple[float, float, float, float],
        margin_mm: float = 0.0,
    ) -> bool:
        o_min_x, o_max_x, o_min_y, o_max_y = outer
        i_min_x, i_max_x, i_min_y, i_max_y = inner
        m = max(0.0, float(margin_mm))
        return (
            i_min_x >= (o_min_x + m)
            and i_max_x <= (o_max_x - m)
            and i_min_y >= (o_min_y + m)
            and i_max_y <= (o_max_y - m)
        )

    @classmethod
    def _buffer_overlap_exceeds_limit(
        cls,
        a: tuple[float, float, float, float],
        b: tuple[float, float, float, float],
    ) -> bool:
        overlap_x, overlap_y = cls._bbox_overlap_xy_mm(a, b)
        if overlap_x <= 0.0 or overlap_y <= 0.0:
            return False
        # Для расположений бок-о-бок или торец-к-торцу важна только ось проникновения.
        penetration = min(overlap_x, overlap_y)
        return penetration > float(getattr(cls, "_MAX_BUFFER_OVERLAP_MM", 100.0))

    # -- валидация и видимость слотов -----------------------------------------

    def _validate_rack_position(
        self: "ModelViewWidget",
        cx: float,
        cy: float,
        zone_id: str,
        params: dict[str, Any],
        rotation: int,
        ignore_rack_id: str | None = None,
    ) -> bool:
        min_x, max_x, min_y, max_y = self._rack_placement_bbox(cx, cy, params, rotation)
        z_ref = self._zone_heights.get(zone_id, (0.0, 0.0))[0]
        corners = [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
        for px, py in corners:
            if not self._point_in_zone_or_boundary(px, py, zone_id, plane_z=z_ref):
                return False
        if self._room_bounds:
            rb = self._room_bounds
            if min_x < rb[0] or max_x > rb[1] or min_y < rb[2] or max_y > rb[3]:
                return False
        cand = self._rack_placement_bbox(cx, cy, params, rotation)
        cand_collision = self._rack_collision_bbox(cx, cy, params, rotation)
        cand_is_mezzanine = self._is_mezzanine_params(params)
        cand_has_aisle = not self._has_additional_model(params)
        cand_aisles = rack_aisle_bboxes(
            cx, cy, params, rotation,
            aisle_mm=self._rack_access_aisle_mm(params),
        ) if cand_has_aisle else []
        cand_columns = self._mezzanine_column_bboxes(cx, cy, params, rotation) if cand_is_mezzanine else []
        for entry in self._rack_entries:
            if entry["zone_id"] != zone_id:
                continue
            if ignore_rack_id and str(entry.get("rack_id", "")) == str(ignore_rack_id):
                continue
            existing_center = entry.get("center") or (0.0, 0.0)
            existing_params = dict(entry.get("params") or {})
            existing_rotation = int(entry.get("rotation", 0))
            ex_x, ex_y = float(existing_center[0]), float(existing_center[1])
            existing_body = self._rack_placement_bbox(ex_x, ex_y, existing_params, existing_rotation)
            existing_collision = self._rack_collision_bbox(ex_x, ex_y, existing_params, existing_rotation)
            existing_is_mezzanine = self._is_mezzanine_params(existing_params)
            existing_has_aisle = not self._has_additional_model(existing_params)
            existing_aisles = rack_aisle_bboxes(
                ex_x, ex_y, existing_params, existing_rotation,
                aisle_mm=self._rack_access_aisle_mm(existing_params),
            ) if existing_has_aisle else []
            existing_columns = self._mezzanine_column_bboxes(
                ex_x, ex_y, existing_params, existing_rotation,
            ) if existing_is_mezzanine else []
            # 1) Пересечение тел по умолчанию запрещено.
            if bboxes_intersect(cand, existing_body):
                if not self._can_share_xy_with_mezzanine(params, existing_params):
                    return False
                if existing_is_mezzanine:
                    # Стеллаж под мезонином должен полностью находиться внутри контура мезонина.
                    if not self._bbox_contains(
                        existing_body,
                        cand,
                        margin_mm=float(getattr(self, "_MEZZANINE_CONTAINMENT_MARGIN_MM", 100.0)),
                    ):
                        return False
                    if not existing_columns:
                        return False
                    if any(bboxes_intersect(cand, col_bbox) for col_bbox in existing_columns):
                        return False
                if cand_is_mezzanine:
                    # Существующий стеллаж должен полностью находиться внутри контура мезонина-кандидата.
                    if not self._bbox_contains(
                        cand,
                        existing_body,
                        margin_mm=float(getattr(self, "_MEZZANINE_CONTAINMENT_MARGIN_MM", 100.0)),
                    ):
                        return False
                    if not cand_columns:
                        return False
                    if any(bboxes_intersect(existing_body, col_bbox) for col_bbox in cand_columns):
                        return False
                    if existing_is_mezzanine and existing_columns:
                        if any(
                            bboxes_intersect(cand_col, ex_col)
                            for cand_col in cand_columns
                            for ex_col in existing_columns
                        ):
                            return False
            # 1.1) Буферные зоны столкновений могут перекрываться до 100 мм (не более).
            if (not cand_is_mezzanine) and (not existing_is_mezzanine):
                if self._buffer_overlap_exceeds_limit(cand_collision, existing_collision):
                    return False
            # 2) Тело кандидата не должно пересекать существующие зоны проходов.
            if existing_aisles:
                if any(bboxes_intersect(cand, ex_aisle) for ex_aisle in existing_aisles):
                    return False
            # 3) Зоны проходов кандидата не должны пересекать существующие тела.
            if (not existing_is_mezzanine) and cand_aisles:
                if any(bboxes_intersect(existing_body, cand_aisle) for cand_aisle in cand_aisles):
                    return False
        return True

    def _on_rack_slot_visibility_changed(
        self: "ModelViewWidget",
        rack_id: str,
        slot_id: str,
        occupied: bool,
    ) -> None:
        self.set_rack_slot_occupied(rack_id, slot_id, occupied)

    def set_rack_slot_occupied(
        self: "ModelViewWidget",
        rack_id: str,
        slot_id: str,
        occupied: bool = True,
    ) -> bool:
        rid = str(rack_id or "")
        sid = str(slot_id or "")
        if not rid or not sid:
            return False
        for entry in self._rack_entries:
            if str(entry.get("rack_id", "")) != rid:
                continue
            actor = (entry.get("slot_actors") or {}).get(sid)
            if actor is None:
                return False
            try:
                actor.SetVisibility(0 if occupied else 1)
            except Exception as exc:
                log_exception(__name__, "set_rack_slot_occupied", exc)
                return False
            hidden = entry.setdefault("hidden_slot_ids", set())
            if occupied:
                hidden.add(sid)
            else:
                hidden.discard(sid)
            if self._plotter:
                self._plotter.update()
            return True
        return False


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Проверка коллизий и ограничений размещения стеллажей.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackCollisionMixin: точки входа
#       Публичные методы сценария:
#       - RackCollisionMixin.set_imported_models_enabled(...)
#       - RackCollisionMixin.set_rack_slot_occupied(...)
#
#    B. RackCollisionMixin: запуск и настройка:
#       RackCollisionMixin.set_imported_models_enabled(...)
#         Назначение: Устанавливает глобальную политику видимости для импортированных пользовательских моделей.
#         Последовательность внутренних вызовов:
#         -> RackCollisionMixin._is_imported_model_entry(...)
#       RackCollisionMixin.set_rack_slot_occupied(...)
#         Назначение: устанавливает rack slot occupied в рамках текущего сценария модуля.
#
#    C. RackCollisionMixin: основной сценарий:
#       RackCollisionMixin._has_additional_model(...)
#         Назначение: проверяет наличие additional model в рамках текущего сценария модуля.
#       RackCollisionMixin._is_imported_model_entry(...)
#         Назначение: проверяет, что imported model entry в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackCollisionMixin._has_additional_model(...)
#       RackCollisionMixin._is_pallet_params(...)
#         Назначение: проверяет, что pallet params в рамках текущего сценария модуля.
#       RackCollisionMixin._validate_rack_position(...)
#         Назначение: проверяет корректность rack position в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackCollisionMixin._rack_placement_bbox(...)
#         -> RackCollisionMixin._rack_collision_bbox(...)
#         -> RackCollisionMixin._has_additional_model(...)
#         -> RackCollisionMixin._buffer_overlap_exceeds_limit(...)
#         -> RackCollisionMixin._rack_access_aisle_mm(...)
#         -> RackCollisionMixin._bbox_contains(...)
#       RackCollisionMixin._on_rack_slot_visibility_changed(...)
#         Назначение: выполняет шаг "on rack slot visibility changed" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackCollisionMixin.set_rack_slot_occupied(...)
#
#    D. RackCollisionMixin: вспомогательные расчёты:
#       RackCollisionMixin._rack_height_mm(...)
#         Назначение: выполняет шаг "rack height mm" в рамках текущего сценария модуля.
#       RackCollisionMixin._force_show_imported_model_entry(...)
#         Назначение: выполняет шаг "force show imported model entry" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackCollisionMixin._is_imported_model_entry(...)
#       RackCollisionMixin._rack_access_aisle_mm(...)
#         Назначение: выполняет шаг "rack access aisle mm" в рамках текущего сценария модуля.
#       RackCollisionMixin._rack_wall_buffer_mm(...)
#         Назначение: выполняет шаг "rack wall buffer mm" в рамках текущего сценария модуля.
#       RackCollisionMixin._rack_placement_bbox(...)
#         Назначение: выполняет шаг "rack placement bbox" в рамках текущего сценария модуля.
#       RackCollisionMixin._rack_collision_buffer_mm(...)
#         Назначение: выполняет шаг "rack collision buffer mm" в рамках текущего сценария модуля.
#       RackCollisionMixin._rack_collision_bbox(...)
#         Назначение: выполняет шаг "rack collision bbox" в рамках текущего сценария модуля.
#       RackCollisionMixin._bbox_overlap_xy_mm(...)
#         Назначение: выполняет шаг "bbox overlap xy mm" в рамках текущего сценария модуля.
#       RackCollisionMixin._bbox_contains(...)
#         Назначение: выполняет шаг "bbox contains" в рамках текущего сценария модуля.
#       RackCollisionMixin._buffer_overlap_exceeds_limit(...)
#         Назначение: выполняет шаг "buffer overlap exceeds limit" в рамках текущего сценария модуля.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
