"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.0 - Task Decorators                                               ║
║  Task 클래스 등록 및 검증 데코레이터                                         ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.0.1                                                             ║
║  Date    : 2026-02-07                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    - rmi_task: Task 클래스 등록 및 메타데이터 설정                           ║
║    - rmi_run: 메인 실행 메서드 표시                                          ║
║    - rmi_signal: Signal 핸들러 등록                                          ║
║    - TaskValidator: Task 클래스 시그니처 검증                                ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

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

# ─────────────────────────────────────────────────────────────────────────────
# 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 자동 감지)"""
    try:
        from PySide6.QtCore import QObject
        if issubclass(cls, QObject):
            return True
    except ImportError:
        pass
    try:
        from PyQt5.QtCore import QObject
        if issubclass(cls, QObject):
            return True
    except ImportError:
        pass
    try:
        from PyQt6.QtCore import QObject
        if issubclass(cls, QObject):
            return True
    except ImportError:
        pass
    return False


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

    Args:
        mode: 실행 모드 (생략 시: QObject→'thread', 그 외→'process')
        name: Task 이름 (생략 시 클래스명 사용)
        restart: 오류 시 자동 재시작 여부
        restart_delay: 재시작 대기 시간(초)
        main_thread: thread 모드에서 메인 스레드에서 인스턴스 생성 (QObject 사용 시 자동 True)
        signal_bridge: RMI Signal → Qt Signal 자동 매핑 딕셔너리 (예: {"sensor.data": "data_changed"})
    """
    __slots__ = ('name', 'mode', 'restart', 'restart_delay', 'main_thread', 'signal_bridge')

    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_bridge: Optional[Dict[str, str]] = None):
        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_bridge = signal_bridge

    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
        # signal_bridge: 데코레이터 파라미터 + 클래스 속성 병합
        if self.signal_bridge:
            existing = getattr(cls, '_rmi_signal_bridge', {}) or {}
            cls._rmi_signal_bridge = {**existing, **self.signal_bridge}
        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 데코레이터가 붙은 메서드 목록 반환"""
    handlers = []
    for name in dir(obj):
        if name.startswith('__'):
            continue  # skip magic methods
        method = getattr(obj, name, None)
        if callable(method) and hasattr(method, '_signal_handler'):
            handlers.append((method._signal_handler, method))
    return handlers


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


class TaskValidator:
    """Task 클래스 검증기"""

    REQUIRED_ATTRS = ['_tc_name', '_tc_mode', '_tc_restart', '_tc_delay']
    ATTR_DISPLAY = {'_tc_name': 'name', '_tc_mode': 'mode', '_tc_restart': 'restart', '_tc_delay': 'restart_delay'}
    VALID_MODES = _VALID_MODES

    def __init__(self):
        self._tasks = []
        self._invalid_tasks = []
        self._missing_attrs = []
        self._invalid_modes = []
        self._missing_run = []
        self._missing_init = []  # QObject 상속 시 __init__ 필수
        self._invalid_init = []
        self._invalid_run_sig = []

    def add(self, task_id: str, task_class_name: str):
        self._tasks.append((task_id, task_class_name))

    def validate(self) -> bool:
        import inspect
        self._invalid_tasks.clear()
        self._missing_attrs.clear()
        self._invalid_modes.clear()
        self._missing_run.clear()
        self._missing_init.clear()
        self._invalid_init.clear()
        self._invalid_run_sig.clear()

        for tid, tcn in self._tasks:
            cls = rmi_task.get(tcn)
            if cls is None:
                self._invalid_tasks.append((tid, tcn))
                continue

            missing = [self.ATTR_DISPLAY[a] for a in self.REQUIRED_ATTRS if not hasattr(cls, a)]
            if missing:
                self._missing_attrs.append((tid, tcn, missing))

            mode = getattr(cls, '_tc_mode', None)
            if mode and mode not in self.VALID_MODES:
                self._invalid_modes.append((tid, tcn, mode))

            if not self._has_run_method(cls):
                self._missing_run.append((tid, tcn))

            # __init__ 검사: QObject 상속 시 필수, 일반 클래스는 생략 허용
            is_qobject = _is_qobject_subclass(cls)
            has_init = '__init__' in cls.__dict__
            if is_qobject and not has_init:
                # QObject는 super().__init__() 호출 필요
                src, line = self._get_source_location(cls)
                self._missing_init.append((tid, tcn, src, line))
            elif has_init:
                params = list(inspect.signature(cls.__init__).parameters.keys())
                if len(params) != 1 or params[0] != 'self':
                    src, line = self._get_source_location(cls, cls.__init__)
                    self._invalid_init.append((tid, tcn, params, src, line))

            run_method, run_name = self._find_run_method(cls)
            if run_method:
                params = list(inspect.signature(run_method).parameters.keys())
                if len(params) != 1 or params[0] != 'self':
                    src, line = self._get_source_location(cls, run_method)
                    self._invalid_run_sig.append((tid, tcn, run_name, params, src, line))

        return not (self._invalid_tasks or self._missing_attrs or self._invalid_modes or
                   self._missing_run or self._missing_init or self._invalid_init or self._invalid_run_sig)

    def _has_run_method(self, cls: type) -> bool:
        has_run = hasattr(cls, 'run') and callable(getattr(cls, 'run', None))
        has_loop = hasattr(cls, 'rmi_loop') and callable(getattr(cls, 'rmi_loop', None))
        has_deco = any(
            getattr(getattr(cls, m, None), '_rmi_run', False) or getattr(getattr(cls, m, None), '_rmi_loop', False)
            for m in dir(cls) if not m.startswith('__'))
        return has_run or has_loop or has_deco

    def _find_run_method(self, cls: type) -> tuple:
        for m in dir(cls):
            if m.startswith('__'): continue
            method = getattr(cls, m, None)
            if callable(method) and (getattr(method, '_rmi_run', False) or getattr(method, '_rmi_loop', False)):
                return (method, m)
        if hasattr(cls, 'run') and callable(getattr(cls, 'run', None)):
            return (getattr(cls, 'run'), 'run')
        if hasattr(cls, 'rmi_loop') and callable(getattr(cls, 'rmi_loop', None)):
            return (getattr(cls, 'rmi_loop'), 'rmi_loop')
        return (None, None)

    def _get_source_location(self, cls: type, method=None) -> tuple:
        import inspect, os
        src, line = None, 0
        try: src = inspect.getfile(cls)
        except: pass
        if src is None:
            try: src = inspect.getsourcefile(cls)
            except: pass
        if src: src = os.path.abspath(src)
        try:
            fn = getattr(method, '__func__', method) if method else cls
            _, line = inspect.getsourcelines(fn)
        except: pass
        return (src, line)

    def print_errors(self):
        import sys
        # ANSI Colors
        GREEN = "\033[92m"  # Bright green for borders
        RED = "\033[31m"
        YELLOW = "\033[33m"
        CYAN = "\033[36m"
        WHITE = "\033[37m"
        BOLD = "\033[1m"
        UNDERLINE = "\033[4m"
        RESET = "\033[0m"

        # Ensure UTF-8 output on Windows
        if sys.platform == 'win32':
            try:
                sys.stdout.reconfigure(encoding='utf-8')
            except Exception:
                pass

        W = 78  # Box width
        B = GREEN  # Border color
        print()
        print(f"{B}╔{'═' * W}╗{RESET}")
        print(f"{B}║{RESET}  {BOLD}{RED}[Validator] ERROR: rmi_task validation failed!{RESET}{' ' * (W - 48)}{B}║{RESET}")
        print(f"{B}╠{'═' * W}╣{RESET}")

        def print_section(title: str, items: list):
            print(f"{B}║{RESET}  {YELLOW}{title}{RESET}{' ' * (W - len(title) - 2)}{B}║{RESET}")
            for item in items:
                # Truncate if too long
                text = item if len(item) <= W - 4 else item[:W - 7] + "..."
                padding = W - len(text) - 4
                print(f"{B}║{RESET}    {WHITE}{text}{RESET}{' ' * padding}{B}║{RESET}")

        def print_link(src: str, line: int):
            if src:
                link = f"{src}:{line}"
                # VSCode clickable link format
                display = f"    → {CYAN}{UNDERLINE}{link}{RESET}"
                # Calculate visible length (without ANSI codes)
                visible_len = 6 + len(link)  # "    → " + link
                padding = W - visible_len
                print(f"{B}║{RESET}{display}{' ' * padding}{B}║{RESET}")

        if self._invalid_tasks:
            items = [f"Task '{tid}': rmi_task '{tcn}' not found" for tid, tcn in self._invalid_tasks]
            print_section("Unregistered rmi_task:", items)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._missing_attrs:
            items = [f"Task '{tid}': rmi_task '{tcn}' missing: {', '.join(attrs)}"
                     for tid, tcn, attrs in self._missing_attrs]
            print_section("Missing rmi_task attributes:", items)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._invalid_modes:
            items = [f"Task '{tid}': rmi_task '{tcn}' mode='{mode}' (must be thread/process)"
                     for tid, tcn, mode in self._invalid_modes]
            print_section("Invalid mode value:", items)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._missing_run:
            items = [f"Task '{tid}': rmi_task '{tcn}' must define run(self)"
                     for tid, tcn in self._missing_run]
            print_section("Missing run() method:", items)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._missing_init:
            print(f"{B}║{RESET}  {YELLOW}Missing __init__ (QObject requires super().__init__):{RESET}{' ' * (W - 54)}{B}║{RESET}")
            for tid, tcn, src, line in self._missing_init:
                text = f"Task '{tid}': rmi_task '{tcn}' (QObject) must define __init__(self)"
                text = text if len(text) <= W - 4 else text[:W - 7] + "..."
                padding = W - len(text) - 4
                print(f"{B}║{RESET}    {WHITE}{text}{RESET}{' ' * padding}{B}║{RESET}")
                print_link(src, line)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._invalid_init:
            print(f"{B}║{RESET}  {YELLOW}Invalid __init__ signature:{RESET}{' ' * (W - 28)}{B}║{RESET}")
            for tid, tcn, params, src, line in self._invalid_init:
                text = f"Task '{tid}': rmi_task '{tcn}' __init__ must be (self), got ({', '.join(params)})"
                text = text if len(text) <= W - 4 else text[:W - 7] + "..."
                padding = W - len(text) - 4
                print(f"{B}║{RESET}    {WHITE}{text}{RESET}{' ' * padding}{B}║{RESET}")
                print_link(src, line)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._invalid_run_sig:
            print(f"{B}║{RESET}  {YELLOW}Invalid @rmi_run method signature:{RESET}{' ' * (W - 36)}{B}║{RESET}")
            for tid, tcn, name, params, src, line in self._invalid_run_sig:
                text = f"Task '{tid}': rmi_task '{tcn}' {name}() must be (self), got ({', '.join(params)})"
                text = text if len(text) <= W - 4 else text[:W - 7] + "..."
                padding = W - len(text) - 4
                print(f"{B}║{RESET}    {WHITE}{text}{RESET}{' ' * padding}{B}║{RESET}")
                print_link(src, line)
            print(f"{B}╟{'─' * W}╢{RESET}")

        # Hint section
        hint = "Hint: def __init__(self) and def run(self)  # runtime is auto-injected"
        print(f"{B}║{RESET}  {CYAN}{hint}{RESET}{' ' * (W - len(hint) - 2)}{B}║{RESET}")
        print(f"{B}╚{'═' * W}╝{RESET}")
        print()

    @property
    def has_errors(self) -> bool:
        return bool(self._invalid_tasks or self._missing_attrs or self._invalid_modes or
                   self._missing_run or self._missing_init or self._invalid_init or self._invalid_run_sig)
