# -*- coding: utf-8 -*-
# gui/containers/percent_sized_widget.py
"""Базовый виджет с процентными размерами и локальной моделью style-наследования."""

from __future__ import annotations

import weakref
from typing import Optional

from PySide6.QtCore import QEvent, QObject, QSize, QTimer, Signal, Slot
from PySide6.QtWidgets import QSizePolicy, QWidget
from shiboken6 import isValid

from error_logger import log_exception
from gui.containers._parent_resize_emitter import ParentResizeEmitter
from gui.containers._widget_style_service import WidgetStyleService
from gui.theme_bus import theme_bus


class PercentSizedWidget(QWidget):
    """Базовый виджет с размерами в процентах от родителя."""

    percent_size_changed = Signal(object)
    _EMITTER_OBJECT_NAME = "_percent_sized_widget_parent_emitter"

    def __init__(
        self,
        width_percent: int | float | None = None,
        height_percent: int | float | None = None,
        parent: QWidget | None = None,
        content_driven: bool = False,
    ):
        super().__init__(parent)
        self._width_percent = self._validate_percent(width_percent, "width_percent")
        self._height_percent = self._validate_percent(height_percent, "height_percent")
        self._content_driven = content_driven
        self.index = 0
        self._target_width: Optional[int] = None
        self._target_height: Optional[int] = None
        self._prev_parent_width = 0
        self._prev_parent_height = 0
        self._update_pending = False
        self._in_update = False
        self._percent_values_dirty = True
        self._parent_emitter: Optional[ParentResizeEmitter] = None
        self._parent_emitter_owner: Optional[QWidget] = None
        self._style_service: Optional[WidgetStyleService] = None
        self._style_parent: Optional[PercentSizedWidget] = None
        self._style_children: weakref.WeakSet[PercentSizedWidget] = weakref.WeakSet()
        self._setup_size_policy()
        self._sync_parent_attachment()
        self.schedule_percent_update()
        self._auto_add_to_parent()
        self._sync_parent_attachment()

    def changeEvent(self, event: QEvent) -> None:
        self._sync_parent_attachment()
        super().changeEvent(event)

    def showEvent(self, event: QEvent) -> None:
        super().showEvent(event)
        self._sync_parent_attachment()
        self._rebind_style_parent()
        self._percent_values_dirty = True
        self.schedule_percent_update()

    def _reattach_to_new_parent(self) -> None:
        self._sync_parent_attachment()

    def _detach_from_current_parent(self) -> None:
        if not self._parent_emitter:
            return
        try:
            self._parent_emitter.parent_resized.disconnect(self._on_parent_resized)
        except RuntimeError as e:
            log_exception(__name__, "_detach_from_current_parent", e)
        try:
            self._parent_emitter.parent_rebuild_finished.disconnect(self._on_parent_rebuild_finished)
        except RuntimeError as e:
            log_exception(__name__, "_detach_from_current_parent.rebuild_finished", e)
        self._parent_emitter = None
        self._parent_emitter_owner = None

    def _attach_to_parent(self, parent: QWidget) -> None:
        emitter = self._find_parent_emitter(parent)
        if not emitter:
            emitter = ParentResizeEmitter(parent)
            emitter.setObjectName(self._EMITTER_OBJECT_NAME)
        emitter.parent_resized.connect(self._on_parent_resized)
        emitter.parent_rebuild_finished.connect(self._on_parent_rebuild_finished)
        self._parent_emitter = emitter
        self._parent_emitter_owner = parent

    def _sync_parent_attachment(self) -> None:
        if not isValid(self):
            return
        try:
            parent = self.parentWidget()
        except RuntimeError as e:
            log_exception(__name__, "_sync_parent_attachment", e)
            return
        if self._parent_emitter_owner is not None and not isValid(self._parent_emitter_owner):
            self._parent_emitter_owner = None
        current_parent = parent if isinstance(parent, QWidget) else None
        if current_parent is self._parent_emitter_owner:
            return
        self._detach_from_current_parent()
        if current_parent is None:
            return
        self._attach_to_parent(current_parent)
        self._rebind_style_parent()
        self._percent_values_dirty = True
        self.schedule_percent_update()

    def _find_parent_emitter(self, parent: QWidget) -> ParentResizeEmitter | None:
        for child in parent.children():
            if not isinstance(child, QObject):
                continue
            if not isinstance(child, ParentResizeEmitter):
                continue
            if child.objectName() == self._EMITTER_OBJECT_NAME:
                return child
        return None

    def on_children_rebuild_finished(self, slot) -> None:
        """Подписать ``slot`` на фазу 2 каскада percent-sized для детей этого виджета.

        Сигнал ``parent_rebuild_finished`` per-parent эмиттера срабатывает после
        того, как дети-PercentSizedWidget ``self`` обработали ``parent_resized``
        (фаза 1) и Qt активировал layout. Это единственная точка, в которой
        виджет-родитель видит стабильную геометрию своих процентных детей.

        Эмиттер создаётся eagerly, чтобы подписка работала и в момент, когда
        у ``self`` ещё нет процентных детей (инициализационный сценарий).
        """
        emitter = self._find_parent_emitter(self)
        if emitter is None:
            emitter = ParentResizeEmitter(self)
            emitter.setObjectName(self._EMITTER_OBJECT_NAME)
        emitter.parent_rebuild_finished.connect(slot)

    def _auto_add_to_parent(self) -> None:
        try:
            parent = self.parentWidget()
        except RuntimeError as e:
            log_exception(__name__, "_auto_add_to_parent", e)
            return
        if not isinstance(parent, QWidget) or not getattr(parent, "_auto_add_children", False):
            return
        if hasattr(parent, "add_widget"):
            try:
                parent.add_widget(self)
                return
            except Exception as e:
                log_exception(__name__, "_auto_add_to_parent.add_widget", e)
        layout = parent.layout()
        if layout is not None and layout.indexOf(self) == -1:
            layout.addWidget(self)

    @staticmethod
    def _validate_percent(percent: int | float | None, param_name: str) -> float | None:
        if percent is None:
            return None
        if not isinstance(percent, (int, float)):
            raise TypeError(f"{param_name} должен быть числом (int/float) или None")
        value = round(float(percent), 2)
        if not 0.01 <= value <= 100.0:
            raise ValueError(f"{param_name} должен быть в диапазоне от 0.01 до 100.00")
        return value

    def _setup_size_policy(self) -> None:
        fallback = QSizePolicy.Policy.Preferred if self._content_driven else QSizePolicy.Policy.Expanding
        horizontal = fallback if self._width_percent is None else QSizePolicy.Policy.Fixed
        vertical = fallback if self._height_percent is None else QSizePolicy.Policy.Fixed
        self.setSizePolicy(horizontal, vertical)

    def sizeHint(self) -> QSize:
        base = super().sizeHint()
        width = self._target_width if self._target_width is not None else base.width()
        height = self._target_height if self._target_height is not None else base.height()
        return QSize(max(0, width), max(0, height))

    def minimumSizeHint(self) -> QSize:
        return QSize(0, 0)

    def set_percent_sizes(
        self,
        width_percent: int | float | None = None,
        height_percent: int | float | None = None,
    ) -> None:
        self._width_percent = self._validate_percent(width_percent, "width_percent")
        self._height_percent = self._validate_percent(height_percent, "height_percent")
        self._percent_values_dirty = True
        self._setup_size_policy()
        self._update_percent_size()

    def set_enabled(self, enabled: bool) -> None: self.setEnabled(enabled)
    def is_enabled(self) -> bool: return self.isEnabled()
    def set_visible(self, visible: bool) -> None: self.setVisible(visible)
    def set_min_width(self, width: int) -> None: self.setMinimumWidth(width)
    def set_min_height(self, height: int) -> None: self.setMinimumHeight(height)
    def set_max_width(self, width: int) -> None: self.setMaximumWidth(width)
    def set_max_height(self, height: int) -> None: self.setMaximumHeight(height)
    def set_tooltip(self, text: str) -> None: self.setToolTip(text)
    def set_size_policy(self, horizontal, vertical) -> None: self.setSizePolicy(horizontal, vertical)
    def get_target_sizes(self) -> tuple[int | None, int | None]: return self._target_width, self._target_height
    def get_percent_sizes(self) -> tuple[float | None, float | None]: return self._width_percent, self._height_percent
    def has_percent_sizing(self) -> bool: return self._width_percent is not None or self._height_percent is not None

    def set_width_pixels(self, width_pixels: int) -> None:
        try:
            parent = self.parentWidget()
        except RuntimeError as e:
            log_exception(__name__, "set_width_pixels", e)
            return
        if not isinstance(parent, QWidget):
            return
        parent_width = max(1, parent.contentsRect().width())
        self.set_percent_sizes(width_percent=round(max(0.01, min(100.0, width_pixels * 100.0 / parent_width)), 2))

    def set_fixed_size(self, width: int, height: int) -> None:
        self.setMinimumSize(width, height)
        self.setMaximumSize(width, height)

    @property
    def width_percent(self) -> float | None: return self._width_percent

    @width_percent.setter
    def width_percent(self, value: int | float | None) -> None: self.set_percent_sizes(width_percent=value)

    @property
    def height_percent(self) -> float | None: return self._height_percent

    @height_percent.setter
    def height_percent(self, value: int | float | None) -> None: self.set_percent_sizes(height_percent=value)

    def _init_stylable(self) -> None:
        self._style_service = WidgetStyleService(self)
        theme_bus.theme_changed.connect(self.set_theme)
        self._rebind_style_parent()

    def _apply_style(self, style_key=None, active_key=None, is_active=None) -> None:
        if self._style_service and self._style_service.apply(style_key=style_key, active_key=active_key, is_active=is_active):
            self._sync_descendant_inherited_styles()

    def style(self, style_key=None, active_key=None, is_active=None) -> None:
        self._apply_style(style_key=style_key, active_key=active_key, is_active=is_active)

    @Slot(str)
    def set_theme(self, theme: str) -> None:
        if self._style_service:
            self._style_service.handle_theme_changed(theme)

    def _sync_inherited_style(self) -> bool:
        if not self._style_service:
            return False
        inherited_style = None
        if self._style_parent and self._style_parent._style_service:
            inherited_style = self._style_parent._style_service.effective_style_key
        return self._style_service.set_inherited_style(inherited_style)

    def _find_style_parent(self) -> PercentSizedWidget | None:
        parent = self.parentWidget()
        while isinstance(parent, QWidget):
            if isinstance(parent, PercentSizedWidget) and parent._style_service:
                return parent
            parent = parent.parentWidget()
        return None

    def _sync_descendant_inherited_styles(self) -> None:
        if not self._style_service:
            return
        inherited_style = self._style_service.effective_style_key
        for child in tuple(self._style_children):
            child._inherit_style_from_parent(inherited_style)

    def _inherit_style_from_parent(self, inherited_style: str | None) -> bool:
        if not self._style_service:
            return False
        if self._style_service.set_inherited_style(inherited_style):
            self._sync_descendant_inherited_styles()
            return True
        return False

    def _rebind_style_parent(self) -> None:
        if not self._style_service:
            return
        new_style_parent = self._find_style_parent()
        if new_style_parent is not self._style_parent:
            if self._style_parent is not None:
                self._style_parent._style_children.discard(self)
            self._style_parent = new_style_parent
            if self._style_parent is not None:
                self._style_parent._style_children.add(self)
        if self._sync_inherited_style():
            self._sync_descendant_inherited_styles()

    def _on_parent_resized(self) -> None:
        self._update_percent_size()

    def _on_parent_rebuild_finished(self) -> None:
        # Фаза 2: родитель завершил собственное перестроение и геометрия стабильна.
        # Форсируем пересчёт даже если parent_width/parent_height численно совпали
        # с промежуточным значением, зафиксированным в фазе 1.
        self._percent_values_dirty = True
        self._update_percent_size()

    def schedule_percent_update(self) -> None:
        if self._update_pending:
            return
        self._update_pending = True
        QTimer.singleShot(0, self._update_percent_size)

    def _trigger_children_update(self) -> None:
        for child in self.children():
            if isinstance(child, PercentSizedWidget) and child.parentWidget() is self:
                child._update_percent_size()

    def _update_percent_size(self) -> None:
        if self._in_update:
            return
        self._in_update = True
        self._update_pending = False
        try:
            self._sync_parent_attachment()
            try:
                parent = self.parentWidget()
            except RuntimeError as e:
                log_exception(__name__, "_update_percent_size", e)
                return
            if not isinstance(parent, QWidget):
                return
            rect = parent.contentsRect()
            parent_width, parent_height = max(0, rect.width()), max(0, rect.height())
            changed = parent_width != self._prev_parent_width or parent_height != self._prev_parent_height
            self._prev_parent_width = parent_width
            self._prev_parent_height = parent_height
            if not changed and not self._percent_values_dirty:
                return
            self._percent_values_dirty = False
            self._target_width = round(parent_width * self._width_percent / 100.0) if self._width_percent is not None else None
            self._target_height = round(parent_height * self._height_percent / 100.0) if self._height_percent is not None else None
            self.updateGeometry()
            layout = parent.layout()
            if layout:
                layout.activate()
            self._trigger_children_update()
        finally:
            self._in_update = False
