# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.0 - Task Decorators                                               ║
║  Task 클래스 등록 및 검증 데코레이터                                         ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.0.1                                                             ║
║  Date    : 2026-02-07                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    - task: Task 데코레이터 (QObject 자동 감지)                               ║
║    - rmi_run: 메인 실행 메서드 표시                                          ║
║    - rmi_signal: Signal 핸들러 등록                                          ║
║    - TaskValidator: Task 클래스 시그니처 검증                                ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

from __future__ import annotations
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

# ─────────────────────────────────────────────────────────────────────────────
# Constants
# ─────────────────────────────────────────────────────────────────────────────
_task_registry: Dict[str, type] = {}
_DEFAULT_RESTART_DELAY = 3.0
_VALID_MODES = ('thread', 'process')


def _is_qobject_subclass(cls: type) -> bool:
    """클래스가 QObject의 서브클래스인지 확인 (PySide6/PyQt5/PyQt6 자동 감지)"""
    for pkg in ('PySide6', 'PyQt5', 'PyQt6'):
        try:
            mod = __import__(f'{pkg}.QtCore', fromlist=['QObject'])
            if issubclass(cls, mod.QObject):
                return True
        except (ImportError, AttributeError):
            pass
    return False


class rmi_task:
    """Task 클래스 등록 데코레이터

    Args:
        mode: 실행 모드 (생략 시: QObject→'thread', 그 외→'process')
        name: Task 이름 (생략 시 클래스명 사용)
        restart: 오류 시 자동 재시작 여부
        restart_delay: 재시작 대기 시간(초)
        main_thread: thread 모드에서 메인 스레드에서 인스턴스 생성 (QObject 사용 시 자동 True)
        signal_forward: RMI Signal → Qt Signal 자동 매핑 딕셔너리 (예: {"sensor.data": "data_changed"})
        signal_subscribe: 자동 구독할 시그널 목록 (예: ["measurement", "sensor.data"])
        debug: 디버그 모드 - True/False 또는 콤마 구분 문자열
               - True: 모든 디버그 출력
               - "signal": 시그널 송수신만 출력
               - "method": RMI 메서드 호출만 출력
               - "variable": RMI 변수 접근만 출력
               - "signal,method": 시그널 + 메서드 호출
               - "signal,method,variable": 전체 출력 (=True)
    """
    __slots__ = ('name', 'mode', 'restart', 'restart_delay', 'main_thread', 'signal_forward', 'signal_subscribe', 'debug')

    def __init__(self, mode: Optional[str] = None, name: Optional[str] = None, restart: bool = True,
                 restart_delay: float = _DEFAULT_RESTART_DELAY, main_thread: bool = False,
                 signal_forward: Optional[Dict[str, str]] = None,
                 signal_subscribe: Optional[List[str]] = None,
                 debug: Any = False):
        self.name = name  # None이면 __call__에서 클래스명 사용
        self.mode = mode  # None이면 __call__에서 결정
        self.restart = restart
        self.restart_delay = restart_delay
        self.main_thread = main_thread
        self.signal_forward = signal_forward
        self.signal_subscribe = signal_subscribe
        self.debug = debug

    def __call__(self, cls):
        name = self.name or cls.__name__  # name 생략 시 클래스명 사용
        is_qobject = _is_qobject_subclass(cls)
        # mode 결정: 생략 시 QObject→thread, 그 외→process
        mode = self.mode
        if mode is None:
            mode = 'thread' if is_qobject else 'process'
        # main_thread 결정: QObject + thread 모드 → 자동 True
        main_thread = self.main_thread
        if not main_thread and mode == 'thread' and is_qobject:
            main_thread = True
        _task_registry[name] = cls
        cls._tc_name = name
        cls._tc_mode = mode
        cls._tc_restart = self.restart
        cls._tc_delay = self.restart_delay
        cls._tc_main_thread = main_thread
        cls._tc_debug = self.debug
        # signal_forward: 데코레이터 파라미터 + 클래스 속성 병합
        if self.signal_forward:
            existing = getattr(cls, '_rmi_signal_forward', {}) or {}
            cls._rmi_signal_forward = {**existing, **self.signal_forward}
        # signal_subscribe: 자동 구독 목록
        if self.signal_subscribe:
            existing = getattr(cls, '_tc_signal_subscribe', []) or []
            cls._tc_signal_subscribe = list(set(existing + self.signal_subscribe))
        return cls

    @staticmethod
    def get(name: str) -> Optional[type]:
        return _task_registry.get(name)

    @staticmethod
    def attr(name: str, key: str, default=None):
        cls = _task_registry.get(name)
        return getattr(cls, key, default) if cls else default

    @staticmethod
    def all() -> Dict[str, type]:
        return _task_registry.copy()


class rmi_run:
    """메인 실행 메서드 표시 (@rmi_run 또는 @rmi_run() 둘 다 지원)"""
    __slots__ = ('_func',)

    def __new__(cls, func=None):
        if func is not None:
            func._rmi_run = True
            return func
        instance = object.__new__(cls)
        instance._func = None
        return instance

    def __call__(self, func):
        func._rmi_run = True
        return func


class rmi_signal:
    """Signal 핸들러 메서드 표시"""
    __slots__ = ('signal_name',)

    def __init__(self, signal_name: str):
        self.signal_name = signal_name

    def __call__(self, func):
        func._signal_handler = self.signal_name
        return func


def has_run_flag(method) -> bool:
    """@rmi_run 데코레이터 플래그 확인"""
    fn = getattr(method, '__func__', method)
    return getattr(fn, '_rmi_run', False) or getattr(fn, '_rmi_loop', False)


def get_signal_handlers(obj) -> List[Tuple[str, Callable]]:
    """@rmi_signal 데코레이터가 붙은 메서드 목록 반환 (Qt 객체 소멸 방어 포함)"""
    handlers = []
    try:
        # dir(obj)나 getattr(obj, ...) 시 이미 소멸된 Qt 객체면 RuntimeError 발생
        for name in dir(obj):
            if name.startswith('__'):
                continue
            try:
                method = getattr(obj, name, None)
                if callable(method) and hasattr(method, '_signal_handler'):
                    handlers.append((method._signal_handler, method))
            except (RuntimeError, AttributeError):
                # 'Internal C++ object already deleted' 또는 속성 접근 에러 무시
                continue
    except (RuntimeError, AttributeError):
        pass
    return handlers


def get_rmi_signal_forward(obj) -> Dict[str, str]:
    """_rmi_signal_forward 속성 반환 (RMI Signal → Qt Signal 매핑)"""
    return getattr(obj, '_rmi_signal_forward', None) or getattr(obj.__class__, '_rmi_signal_forward', {})


from ._task_validator import TaskValidator  # noqa: E402 — 분리된 모듈 re-import


# ─────────────────────────────────────────────────────────────────────────────
# @task - Unified Task Decorator
# ─────────────────────────────────────────────────────────────────────────────

def _is_qt_signal(attr) -> bool:
    """Qt Signal인지 확인 (PySide6/PyQt5/PyQt6)"""
    attr_type = type(attr).__name__
    # PySide6: SignalInstance, PyQt5/6: pyqtSignal bound
    return attr_type in ('SignalInstance', 'pyqtBoundSignal', 'pyqtSignal')


def task(cls=None, *, name: Optional[str] = None, mode: Optional[str] = None,
         restart: bool = False, debug: Union[bool, str] = False, **kwargs):
    """통합 Task 데코레이터 - QObject 상속 시 자동으로 thread 모드

    다음을 자동 처리합니다:
    - QObject 상속 감지 → thread 모드 강제
    - QtSignal 자동 감지 → signal_forward 매핑
    - on_* 메서드 자동 감지 → signal_subscribe
    - run 메서드 자동 @rmi_run 적용

    Args:
        name: Task 이름 (생략 시 클래스명 소문자)
        mode: 실행 모드 ('thread' 또는 'process')
              QObject 상속 시 무시되고 'thread' 강제
              생략 시 기본값 'thread'
        restart: 오류 시 자동 재시작 여부 (기본: False)
        debug: 디버그 모드 - True/False 또는 콤마 구분 문자열
               - True: 모든 디버그 출력
               - "signal": 시그널 송수신만 출력
               - "method": RMI 메서드 호출만 출력
               - "variable": RMI 변수 접근만 출력
               - "signal,method": 시그널 + 메서드 호출
               - "signal,method,variable": 전체 출력 (=True)
        **kwargs: 추가 인수 (restart_delay, main_thread, signal_forward 등)

    Example:
        # Non-Qt Task
        @task(mode="process", restart=True)
        class DataProcessor:
            def run(self):  # @rmi_run 자동 적용
                while self.running:  # should_stop() 대신
                    pass

            def on_data_ready(self, signal):  # 자동 구독
                pass

        # Qt Task (QObject 자동 감지)
        @task(restart=True)
        class ViewerWidget(QMainWindow):  # thread 모드 자동
            frame_ready = Signal(object)  # signal_forward 자동

            @ui_thread
            def on_frame_ready(self, signal):  # 자동 구독
                self.label.setText(str(signal.data))
    """
    def decorator(cls):
        task_name = name or cls.__name__
        actual_mode = mode

        # QObject 상속 감지
        is_qt = _is_qobject_subclass(cls)

        if is_qt:
            # Qt 객체는 항상 thread 모드 (pickle 불가)
            if mode and mode != 'thread':
                import warnings
                warnings.warn(
                    f"@task({cls.__name__}): QObject는 pickle 불가로 thread 모드만 지원. "
                    f"mode='{mode}' 무시됨.",
                    UserWarning
                )
            actual_mode = "thread"

            # QtSignal 자동 감지 → signal_forward
            signal_forward = kwargs.get('signal_forward', {}).copy()
            for attr_name in dir(cls):
                if attr_name.startswith('_'):
                    continue
                attr = getattr(cls, attr_name, None)
                if _is_qt_signal(attr):
                    # frame_ready → frame.ready
                    signal_name = attr_name.replace('_', '.')
                    if signal_name not in signal_forward:
                        signal_forward[signal_name] = attr_name

            if signal_forward:
                kwargs['signal_forward'] = signal_forward
        else:
            actual_mode = mode or "thread"

        # run 메서드 자동 @rmi_run 적용
        if hasattr(cls, 'run'):
            run_method = cls.run
            # 이미 @rmi_run 적용 여부 확인
            if not getattr(run_method, '_rmi_run', False):
                cls.run = rmi_run(run_method)

        # on_* 메서드 자동 감지 → signal_subscribe
        signals = list(kwargs.get('signal_subscribe', []))
        for attr_name in dir(cls):
            if attr_name.startswith('on_') and not attr_name.startswith('on__'):
                method = getattr(cls, attr_name, None)
                if callable(method):
                    # on_frame_ready → frame.ready
                    signal_name = attr_name[3:].replace('_', '.')
                    if signal_name not in signals:
                        signals.append(signal_name)

        if signals:
            kwargs['signal_subscribe'] = signals

        # self.running, self.task_name 프로퍼티 추가
        if not hasattr(cls, 'running'):
            cls.running = property(lambda self: getattr(self, 'runtime', None) and self.runtime.running)
        if not hasattr(cls, 'task_name'):
            cls.task_name = property(lambda self: getattr(self, 'runtime', None) and self.runtime.name)

        # DeviceProperty 지원 추가
        if hasattr(cls, 'DEVICE_PROPERTY'):
            try:
                from ..device.device_schema import parse_key, parse_condition
                from ..device.device_property import DeviceProperty
                from ..device.device_infra import DeviceTaskMixin
            except ImportError as e:
                print(f"[task_decorator] Failed to import device components: {e}")
                raise
            import inspect

            # 1. DeviceTaskMixin 주입
            if not issubclass(cls, DeviceTaskMixin):
                for name_attr in dir(DeviceTaskMixin):
                    if name_attr.startswith("__") and name_attr.endswith("__"):
                        continue
                    # 메서드뿐만 아니라 속성(descriptor 등)도 복사
                    try:
                        attr_val = getattr(DeviceTaskMixin, name_attr)
                        if not hasattr(cls, name_attr):
                            setattr(cls, name_attr, attr_val)
                    except AttributeError:
                        continue
                
                # 타입 힌트로만 존재하는 필드들에 대한 초기값 설정
                for field in ["_resync_open_fn", "_resync_close_fn"]:
                    if not hasattr(cls, field):
                        setattr(cls, field, None)

            schema = getattr(cls, 'DEVICE_PROPERTY')
            if not isinstance(schema, dict):
                raise TypeError(f"[{cls.__name__}] DEVICE_PROPERTY must be a dict")

            # 2. @resync 해석
            resync_list = schema.get("@resync", {})
            if isinstance(resync_list, dict):
                resync_list = [resync_list]
            
            # 속성별 (condition, order) 맵 구축
            dep_map = {}
            for group in resync_list:
                cond_raw = group.get("condition")
                if not cond_raw: continue
                
                try:
                    cond_fn = parse_condition(cond_raw)
                except Exception as e:
                    raise ValueError(f"[{cls.__name__}] Invalid @resync condition: {e}")
                
                order_list = group.get("order", [])
                for i, entry in enumerate(order_list):
                    names = [entry] if isinstance(entry, str) else entry
                    for dep_name in names:
                        dep_map[dep_name] = (cond_fn, i)
                
                # Resync open/close 메서드명 저장
                if "open" in group: cls._resync_open_fn = group["open"]
                if "close" in group: cls._resync_close_fn = group["close"]

            # 3. 속성 생성 및 검증
            for key, defn in schema.items():
                if key.startswith("@"): continue
                
                p_name, p_dtype, p_default = parse_key(key)
                opstate, order = dep_map.get(p_name, (None, 0))
                
                # DeviceProperty 생성
                dp = DeviceProperty(
                    dtype=p_dtype,
                    default=p_default,
                    validator=defn.get("validator"),
                    setter=defn.get("setter"),
                    getter=defn.get("getter"),
                    opstate=opstate,
                    order=order,
                    readonly=defn.get("readonly", False),
                    debounce=defn.get("debounce", 0),
                    per_sync=defn.get("per_sync", False),
                    notify_mode=defn.get("notify_mode", "immediate")
                )
                dp.__set_name__(cls, p_name) # 수동 호출 필수 (setattr은 자동 호출 안함)
                setattr(cls, p_name, dp)

            # 4. 정적 검증 (콜백 존재 여부 및 시그니처)
            for attr_name in dir(cls):
                attr = getattr(cls, attr_name)
                if isinstance(attr, DeviceProperty):
                    # Setter 검증
                    if isinstance(attr.setter_ref, str):
                        if not hasattr(cls, attr.setter_ref):
                            raise AttributeError(f"[{cls.__name__}] setter '{attr.setter_ref}' not found for property '{attr_name}'")
                        method = getattr(cls, attr.setter_ref)
                        sig = inspect.signature(method)
                        if len(sig.parameters) != 2: # self, value
                            raise TypeError(f"[{cls.__name__}] setter '{attr.setter_ref}' must have 2 parameters (self, value)")
                    
                    # Getter 검증
                    if isinstance(attr.getter_ref, str):
                        if not hasattr(cls, attr.getter_ref):
                            raise AttributeError(f"[{cls.__name__}] getter '{attr.getter_ref}' not found for property '{attr_name}'")
                        method = getattr(cls, attr.getter_ref)
                        sig = inspect.signature(method)
                        if len(sig.parameters) != 1: # self
                            raise TypeError(f"[{cls.__name__}] getter '{attr.getter_ref}' must have 1 parameter (self)")

                    # Validator 검증
                    if isinstance(attr.validator_ref, str):
                        if not hasattr(cls, attr.validator_ref):
                            raise AttributeError(f"[{cls.__name__}] validator '{attr.validator_ref}' not found for property '{attr_name}'")
                        method = getattr(cls, attr.validator_ref)
                        sig = inspect.signature(method)
                        if len(sig.parameters) != 2: # self, value
                            raise TypeError(f"[{cls.__name__}] validator '{attr.validator_ref}' must have 2 parameters (self, value)")

        # rmi_task 적용
        return rmi_task(
            name=task_name,
            mode=actual_mode,
            restart=restart,
            debug=debug,
            **kwargs
        )(cls)

    # @task 또는 @task(...) 둘 다 지원
    if cls is not None:
        return decorator(cls)
    return decorator
