# -*- coding: utf-8 -*-
"""Вспомогательные функции кодов стеллажей — утилиты именования, нумерации и определения стиля."""

from __future__ import annotations

import re
from typing import Any, TYPE_CHECKING
from error_logger import log_exception

if TYPE_CHECKING:
    from gui.components.model_view_widget import ModelViewWidget


class RackCodesMixin:
    """Чисто логические утилиты для работы с кодами стеллажей (без зависимостей от Qt / VTK)."""

    @staticmethod
    def _split_code_parts(value: Any) -> tuple[str, str]:
        raw = str(value or "").strip().upper()
        if raw.startswith("MEZO_"):
            suffix = "".join(ch for ch in raw[5:] if "A" <= ch <= "Z")
            return "MEZO_", suffix
        letters = "".join(ch for ch in raw if "A" <= ch <= "Z")
        return "", letters

    @staticmethod
    def _code_prefix_for_params(params: dict[str, Any]) -> str:
        rack_type = str(dict(params or {}).get("rack_type") or "").strip().lower()
        if rack_type in {"mezo", "mezz", "mezzanine"}:
            return "MEZO_"
        additional_model = dict(dict(params or {}).get("additional_model") or {})
        explicit = additional_model.get("is_mezzanine")
        if isinstance(explicit, bool):
            return "MEZO_" if explicit else ""
        model_kind = str(additional_model.get("model_kind") or "").strip().lower()
        if model_kind in {"mezo", "mezz", "mezzanine"}:
            return "MEZO_"
        haystack = " ".join(
            str(v)
            for v in (
                additional_model.get("model_name"),
                additional_model.get("model_path"),
                rack_type,
            )
            if v
        ).lower()
        if any(tag in haystack for tag in ("mezo", "mezz", "Р СР ВµР В·", "mezan", "mezon")):
            return "MEZO_"
        return ""

    @classmethod
    def _normalize_rack_code(
        cls,
        value: Any,
        fallback: str = "A",
        prefix: str = "",
    ) -> str:
        _given_prefix, letters = cls._split_code_parts(value)
        if not letters:
            _fb_prefix, fb_letters = cls._split_code_parts(fallback)
            letters = fb_letters
        if not letters:
            letters = "A"
        if str(prefix or "").upper() == "MEZO_":
            return f"MEZO_{letters}"
        return letters

    @staticmethod
    def _normalize_numbering_direction(value: Any) -> str:
        direction = str(value or "").strip().lower()
        if direction in {"right_to_left", "rtl"}:
            return "right_to_left"
        return "left_to_right"

    @staticmethod
    def _canonical_center_span(rack_type: Any, span_width_mm: Any, fallback_center_mm: Any) -> int:
        kind = str(rack_type or "").strip().upper()
        width = int(span_width_mm)
        fallback = int(fallback_center_mm)
        if kind == "A":
            if width == 1000:
                return 1033
            if width == 1300:
                return 1333
        if kind == "B":
            if width == 900:
                return 950
            if width == 1200:
                return 1250
            if width == 1500:
                return 1550
        return fallback

    @classmethod
    def _build_span_codes(cls, code: str, spans_count: int, numbering_direction: str) -> list[str]:
        prefix, letters = cls._split_code_parts(code)
        base = letters or "A"
        count = max(1, int(spans_count or 1))
        ordered = [f"{prefix}{base}{idx:02d}" for idx in range(1, count + 1)]
        if cls._normalize_numbering_direction(numbering_direction) == "right_to_left":
            return list(reversed(ordered))
        return ordered

    @classmethod
    def _letters_to_number(cls, letters: str) -> int:
        _prefix, clean = cls._split_code_parts(letters)
        value = 0
        for ch in clean:
            value = value * 26 + (ord(ch) - ord("A") + 1)
        return value

    @staticmethod
    def _number_to_letters(number: int) -> str:
        n = max(1, int(number))
        out = []
        while n > 0:
            n -= 1
            out.append(chr(ord("A") + (n % 26)))
            n //= 26
        return "".join(reversed(out))

    def _next_zone_rack_code(self: "ModelViewWidget", zone_id: str, params: dict[str, Any] | None = None) -> str:
        zid = str(zone_id or "")
        source_params = dict(params or {})
        wanted_prefix = self._code_prefix_for_params(source_params)
        used_numbers: set[int] = set()
        for entry in self._rack_entries:
            if zid and str(entry.get("zone_id", "")) != zid:
                continue
            entry_params = dict(entry.get("params") or {})
            code = str(entry.get("code") or entry_params.get("code") or "")
            if not code:
                continue
            entry_prefix = self._code_prefix_for_params(entry_params)
            if not entry_prefix:
                parsed_prefix, _letters = self._split_code_parts(code)
                entry_prefix = parsed_prefix
            if str(entry_prefix or "") != str(wanted_prefix or ""):
                continue
            code_num = int(self._letters_to_number(code))
            if code_num > 0:
                used_numbers.add(code_num)

        # Сохраняем ручной код, если он свободен в целевой зоне/префиксе.
        preferred_code = self._normalize_rack_code(
            source_params.get("code"),
            fallback="MEZO_A" if wanted_prefix else "A",
            prefix=wanted_prefix,
        )
        preferred_num = int(self._letters_to_number(preferred_code))
        if preferred_num > 0 and preferred_num not in used_numbers:
            if wanted_prefix:
                return f"{wanted_prefix}{self._number_to_letters(preferred_num)}"
            return self._number_to_letters(preferred_num)

        # Автоматический режим: выбираем ближайший свободный слот, начиная с A.
        next_num = 1
        while next_num in used_numbers:
            next_num += 1
        next_letters = self._number_to_letters(next_num)
        if wanted_prefix:
            return f"{wanted_prefix}{next_letters}"
        return next_letters

    def _normalize_rack_params(
        self: "ModelViewWidget",
        params: dict[str, Any],
        fallback_code: str | None = None,
    ) -> dict[str, Any]:
        normalized = dict(params or {})
        prefix = self._code_prefix_for_params(normalized)
        default_code = "MEZO_A" if prefix else "A"
        code = self._normalize_rack_code(
            normalized.get("code"),
            fallback=fallback_code or default_code,
            prefix=prefix,
        )
        spans_count = max(1, int(normalized.get("spans_count", 1)))
        raw_span_widths = [int(v) for v in (normalized.get("span_widths_mm") or []) if int(v) > 0]
        if not raw_span_widths:
            raw_span_widths = [int(normalized.get("footprint_width_mm", 1000))]
        if len(raw_span_widths) < spans_count:
            raw_span_widths.extend([raw_span_widths[-1]] * (spans_count - len(raw_span_widths)))
        raw_span_widths = raw_span_widths[:spans_count]

        if self._is_pallet_params(normalized):
            center_spans = [self._PALLET_CENTER_SPAN_MM for _ in range(spans_count)]
        else:
            center_spans = [int(v) for v in (normalized.get("center_spans_mm") or raw_span_widths) if int(v) > 0]
            if not center_spans:
                center_spans = list(raw_span_widths)
            if len(center_spans) < spans_count:
                center_spans.extend([center_spans[-1]] * (spans_count - len(center_spans)))
            center_spans = center_spans[:spans_count]
            rack_type = str(normalized.get("rack_type") or "")
            center_spans = [
                self._canonical_center_span(rack_type, raw_span_widths[i], center_spans[i])
                for i in range(spans_count)
            ]

        if self._is_pallet_params(normalized):
            raw_span_widths = [self._PALLET_FOOTPRINT_WIDTH_SINGLE_MM for _ in range(spans_count)]
            footprint_width = int(
                sum(center_spans)
                + (self._PALLET_FOOTPRINT_WIDTH_SINGLE_MM - self._PALLET_CENTER_SPAN_MM)
            )
            normalized["depth_mm"] = int(self._PALLET_DEPTH_MM)
            normalized["footprint_depth_mm"] = int(self._PALLET_DEPTH_MM)
            normalized["footprint_height_mm"] = int(self._PALLET_HEIGHT_MM)
        else:
            footprint_width = int(sum(center_spans))
        numbering_direction = self._normalize_numbering_direction(
            normalized.get("numbering_direction", "left_to_right")
        )
        normalized["code"] = code
        normalized["spans_count"] = spans_count
        normalized["span_widths_mm"] = raw_span_widths
        normalized["center_spans_mm"] = center_spans
        normalized["footprint_width_mm"] = footprint_width
        normalized["numbering_direction"] = numbering_direction
        normalized["span_codes"] = self._build_span_codes(code, spans_count, numbering_direction)
        return normalized

    def _resolve_rack_style(
        self: "ModelViewWidget",
        params: dict[str, Any],
        fallback_color: str = "#7FB3D5",
        fallback_opacity: float = 0.9,
    ) -> tuple[str, float]:
        color = str(params.get("display_color") or fallback_color).strip()
        opacity = params.get("display_opacity", fallback_opacity)
        try:
            opacity = float(opacity)
        except Exception as exc:
            log_exception(__name__, "_resolve_rack_style", exc)
            opacity = float(fallback_opacity)
        opacity = max(0.05, min(1.0, opacity))
        match = re.fullmatch(r"#([0-9A-Fa-f]{8})", color)
        if match:
            hex_rgba = match.group(1)
            color = f"#{hex_rgba[2:]}"
            try:
                opacity = max(0.05, min(1.0, int(hex_rgba[:2], 16) / 255.0))
            except Exception as _exc:
                log_exception(__name__, "_resolve_rack_style", _exc)
        if not re.fullmatch(r"#([0-9A-Fa-f]{6})", color):
            color = fallback_color
        return color, opacity

    def _apply_rack_shelf_opacity(self: "ModelViewWidget", rack_id: str) -> None:
        """Устанавливает полную непрозрачность актёров стеллажа при наличии полок, иначе восстанавливает исходную."""
        rid = str(rack_id or "")
        if not rid:
            return
        entry = self._get_rack_entry(rid)
        if entry is None:
            return
        slot_actor_ids = {
            id(actor)
            for actor in (entry.get("slot_actors") or {}).values()
            if actor is not None
        }
        has_shelves = any(str(k[0]) == rid for k in self._rack_shelf_params)
        if has_shelves:
            target_opacity = 1.0
        else:
            params = dict(entry.get("params") or {})
            _color, target_opacity = self._resolve_rack_style(params)
        for actor in entry.get("actors", []):
            try:
                if id(actor) in slot_actor_ids:
                    # Прокси-объекты слотов управляются логикой размещения полок.
                    continue
                prop = actor.GetProperty()
                if prop is not None:
                    prop.SetOpacity(target_opacity)
            except Exception as _exc:
                log_exception(__name__, "_apply_rack_shelf_opacity", _exc)
        for (entry_rid, slot_id), actors in (self._rack_shelf_actors or {}).items():
            if str(entry_rid) != rid:
                continue
            for idx, actor in enumerate(list(actors or []), start=1):
                if actor is None:
                    continue
                try:
                    prop = actor.GetProperty()
                    if prop is not None:
                        slot_opacity = float(self._shelf_cell_visual_opacity(rid, str(slot_id), idx))
                        prop.SetOpacity(float(slot_opacity))
                except Exception as _exc:
                    log_exception(__name__, "_apply_rack_shelf_opacity", _exc)
    def _shelf_cell_visual_opacity(
        self: "ModelViewWidget",
        rack_id: str,
        slot_id: str,
        shelf_index: int | None = None,
    ) -> float:
        rid = str(rack_id or "")
        sid = str(slot_id or "")
        if not rid or not sid:
            return 0.65
        if shelf_index is not None:
            idx = max(1, int(shelf_index or 1))
            indexed_storage = dict(getattr(self, "_rack_shelf_cell_links_indexed", {}) or {})
            has_any_indexed = any(str(k[0]) == rid and str(k[1]) == sid for k in indexed_storage.keys())
            if has_any_indexed:
                linked_idx = bool(indexed_storage.get((rid, sid, idx), False))
                return 1.0 if linked_idx else 0.65
        linked = bool((self._rack_shelf_cell_links or {}).get((rid, sid), False))
        return 1.0 if linked else 0.65

    def set_shelf_cell_marker(
        self: "ModelViewWidget",
        rack_id: str,
        slot_id: str,
        linked: bool,
    ) -> None:
        rid = str(rack_id or "")
        sid = str(slot_id or "")
        if not rid or not sid:
            return
        self._rack_shelf_cell_links[(rid, sid)] = bool(linked)
        self._apply_rack_shelf_opacity(rid)
        if self._plotter:
            self._plotter.update()

    def set_rack_shelf_cell_markers(
        self: "ModelViewWidget",
        rack_id: str,
        slot_markers: dict[str, bool],
    ) -> None:
        rid = str(rack_id or "")
        if not rid:
            return
        markers = dict(slot_markers or {})
        keys_to_drop = [key for key in (self._rack_shelf_cell_links or {}).keys() if str(key[0]) == rid]
        for key in keys_to_drop:
            self._rack_shelf_cell_links.pop(key, None)
        for sid, linked in markers.items():
            slot_id = str(sid or "")
            if not slot_id:
                continue
            self._rack_shelf_cell_links[(rid, slot_id)] = bool(linked)
        self._apply_rack_shelf_opacity(rid)
        if self._plotter:
            self._plotter.update()

    def set_rack_shelf_cell_markers_indexed(
        self: "ModelViewWidget",
        rack_id: str,
        slot_index_markers: dict[str, bool],
    ) -> None:
        """Точечные маркеры размещения ячеек на уровне конкретной полки (slot + index)."""
        rid = str(rack_id or "")
        if not rid:
            return
        markers = dict(slot_index_markers or {})
        keys_to_drop = [key for key in (self._rack_shelf_cell_links_indexed or {}).keys() if str(key[0]) == rid]
        for key in keys_to_drop:
            self._rack_shelf_cell_links_indexed.pop(key, None)
        for key, linked in markers.items():
            raw = str(key or "")
            if "::" not in raw:
                continue
            slot_id, raw_idx = raw.split("::", 1)
            slot_id = str(slot_id or "")
            if not slot_id:
                continue
            try:
                idx = max(1, int(raw_idx))
            except Exception as exc:
                log_exception(__name__, "set_rack_shelf_cell_markers_indexed", exc)
                continue
            self._rack_shelf_cell_links_indexed[(rid, slot_id, idx)] = bool(linked)
        self._apply_rack_shelf_opacity(rid)
        if self._plotter:
            self._plotter.update()


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Нормализация и генерация кодов стеллажей и Секций.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackCodesMixin: точки входа
#       Публичные методы отсутствуют; сценарий запускается через методы родительских модулей и внутренние обработчики.
#
#    B. RackCodesMixin: основной сценарий:
#       RackCodesMixin._resolve_rack_style(...)
#         Назначение: определяет rack style в рамках текущего сценария модуля.
#       RackCodesMixin._apply_rack_shelf_opacity(...)
#         Назначение: Устанавливает полную непрозрачность актёров стеллажа при наличии полок, иначе восстанавливает исходную.
#         Последовательность внутренних вызовов:
#         -> RackCodesMixin._resolve_rack_style(...)
#
#    C. RackCodesMixin: вспомогательные расчёты:
#       RackCodesMixin._split_code_parts(...)
#         Назначение: разделяет code parts в рамках текущего сценария модуля.
#       RackCodesMixin._code_prefix_for_params(...)
#         Назначение: выполняет шаг "code prefix for params" в рамках текущего сценария модуля.
#       RackCodesMixin._normalize_rack_code(...)
#         Назначение: нормализует rack code в рамках текущего сценария модуля.
#       RackCodesMixin._normalize_numbering_direction(...)
#         Назначение: нормализует numbering direction в рамках текущего сценария модуля.
#       RackCodesMixin._canonical_center_span(...)
#         Назначение: выполняет шаг "canonical center span" в рамках текущего сценария модуля.
#       RackCodesMixin._build_span_codes(...)
#         Назначение: строит span codes в рамках текущего сценария модуля.
#       RackCodesMixin._letters_to_number(...)
#         Назначение: выполняет шаг "letters to number" в рамках текущего сценария модуля.
#       RackCodesMixin._number_to_letters(...)
#         Назначение: выполняет шаг "number to letters" в рамках текущего сценария модуля.
#       RackCodesMixin._next_zone_rack_code(...)
#         Назначение: выполняет шаг "next zone rack code" в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackCodesMixin._code_prefix_for_params(...)
#         -> RackCodesMixin._normalize_rack_code(...)
#         -> RackCodesMixin._number_to_letters(...)
#         -> RackCodesMixin._letters_to_number(...)
#         -> RackCodesMixin._split_code_parts(...)
#       RackCodesMixin._normalize_rack_params(...)
#         Назначение: нормализует rack params в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> RackCodesMixin._code_prefix_for_params(...)
#         -> RackCodesMixin._normalize_rack_code(...)
#         -> RackCodesMixin._normalize_numbering_direction(...)
#         -> RackCodesMixin._build_span_codes(...)
#         -> RackCodesMixin._canonical_center_span(...)
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).

