"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.0 - Task Signal                                                   ║
║  Process-Safe Signal Communication Module                                    ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.0.1                                                             ║
║  Date    : 2026-02-07                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    프로세스 간 안전한 시그널 통신 시스템                                     ║
║    - Pub/Sub 패턴 기반 이벤트 브로드캐스트                                   ║
║    - Thread/Process 혼합 모드 지원                                           ║
║    - Pickle 가능한 데이터만 IPC 전송 가능                                    ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Classes:                                                                    ║
║    Signal       - 시그널 메시지 데이터 클래스                                ║
║    SignalBroker - 큐/구독 관리 중앙 브로커                                   ║
║    SignalClient - Task용 시그널 송수신 래퍼                                  ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

from __future__ import annotations
from typing import Any, Dict, List, Callable, Optional, TYPE_CHECKING
from dataclasses import dataclass, field
import threading
import queue
import time
import uuid
import pickle
import traceback
from datetime import datetime

if TYPE_CHECKING:
    from multiprocessing.managers import SyncManager

# ─────────────────────────────────────────────────────────────────────────────
# Constants
# ─────────────────────────────────────────────────────────────────────────────
_RECV_TIMEOUT = 0.5
_STOP_TIMEOUT = 1.0


def _is_picklable(obj: Any) -> bool:
    """객체가 pickle 가능한지 확인"""
    try:
        pickle.dumps(obj)
        return True
    except (pickle.PicklingError, TypeError, AttributeError):
        return False


@dataclass
class Signal:
    name: str
    source: str
    data: Any = None
    signal_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    timestamp: float = field(default_factory=time.time)

    def validate_for_ipc(self) -> bool:
        """IPC(프로세스 간 통신)에 사용 가능한지 확인"""
        return _is_picklable(self.data)


class SignalBroker:
    """시그널 브로커. mgr=None: thread 모드, mgr=Manager(): process 모드 (IPC)"""
    __slots__ = ('_mgr', '_queues', '_subscribers', '_lock', '_mode', '_local_cache', '_local_lock', '_stats', '_stats_enabled', '_stats_cleared_at')

    def __init__(self, mgr: Optional[SyncManager] = None):
        self._mgr = mgr
        self._mode = 'process' if mgr else 'thread'
        # Process 모드: Manager dict (프로세스 간 공유), Thread 모드: 일반 dict
        if mgr:
            self._queues = mgr.dict()
            self._subscribers = mgr.dict()
            self._subscribers['__version__'] = 0  # 버전 카운터 (캐시 무효화용)
            self._lock = mgr.Lock()
            self._stats = mgr.dict()  # 시그널 통계: {signal_name: {'count': int, 'last_time': float}}
            self._stats_enabled = mgr.Value('b', True)  # 통계 수집 on/off
            self._stats_cleared_at = mgr.Value('d', 0.0)  # 마지막 클리어 시간
        else:
            self._queues: Dict[str, Any] = {}
            self._subscribers: Dict[str, List[str]] = {}
            self._lock = threading.Lock()
            self._stats: Dict[str, Dict[str, Any]] = {}
            self._stats_enabled = True
            self._stats_cleared_at = 0.0
        # 로컬 캐시 (IPC 호출 최소화)
        self._local_cache: Dict[str, Any] = {'version': -1}
        self._local_lock = threading.Lock()

    def __getstate__(self):
        """Pickle 직렬화: _mgr 제외 (weakref 포함)"""
        return {
            '_mode': self._mode,
            '_queues': self._queues,
            '_subscribers': self._subscribers,
            '_lock': self._lock,
            '_stats': self._stats,
            '_stats_enabled': self._stats_enabled,
            '_stats_cleared_at': self._stats_cleared_at,
        }

    def __setstate__(self, state):
        """Pickle 역직렬화: _mgr=None으로 복원, 로컬 캐시 초기화"""
        self._mgr = None  # Manager는 복원 불가, 새 queue 생성 불가
        self._mode = state['_mode']
        self._queues = state['_queues']
        self._subscribers = state['_subscribers']
        self._lock = state['_lock']
        self._stats = state['_stats']
        self._stats_enabled = state['_stats_enabled']
        self._stats_cleared_at = state['_stats_cleared_at']
        # 로컬 캐시 초기화 (버전 -1로 강제 리프레시)
        self._local_cache = {'version': -1}
        self._local_lock = threading.Lock()

    def register(self, task_id: str) -> Any:
        """태스크 큐 등록 (모드에 따라 적절한 큐 타입 생성)"""
        queues = self._queues
        with self._lock:
            if task_id not in queues:
                if self._mgr:
                    queues[task_id] = self._mgr.Queue()
                else:
                    queues[task_id] = queue.Queue()
            return queues[task_id]

    @property
    def mode(self) -> str:
        """현재 모드 반환 ('thread' 또는 'process')"""
        return self._mode

    def unregister(self, task_id: str):
        with self._lock:
            self._queues.pop(task_id, None)
            # 새 구조: ":task_id"로 끝나는 모든 키 제거
            suffix = f":{task_id}"
            keys_to_remove = [k for k in list(self._subscribers.keys()) if k.endswith(suffix) and k != '__version__']
            for k in keys_to_remove:
                self._subscribers.pop(k, None)
            # 버전 카운터 증가 (다른 프로세스 캐시 무효화)
            if self._mode == 'process' and keys_to_remove:
                self._subscribers['__version__'] = self._subscribers.get('__version__', 0) + 1
        # 로컬 캐시 무효화
        with self._local_lock:
            self._local_cache.pop('subscribers_keys', None)

    def subscribe(self, signal_name: str, task_id: str):
        # 단순화: (signal_name, task_id) -> True 형태로 저장
        key = f"{signal_name}:{task_id}"
        with self._lock:
            self._subscribers[key] = True
            # 버전 카운터 증가 (다른 프로세스 캐시 무효화)
            if self._mode == 'process':
                self._subscribers['__version__'] = self._subscribers.get('__version__', 0) + 1
        # 로컬 캐시 무효화 (스레드 모드용)
        with self._local_lock:
            self._local_cache.pop('subscribers_keys', None)
        print(f"[SignalBroker] subscribe: {signal_name} <- {task_id}")

    def unsubscribe(self, signal_name: str, task_id: str):
        key = f"{signal_name}:{task_id}"
        with self._lock:
            self._subscribers.pop(key, None)
            # 버전 카운터 증가 (다른 프로세스 캐시 무효화)
            if self._mode == 'process':
                self._subscribers['__version__'] = self._subscribers.get('__version__', 0) + 1
        # 로컬 캐시 무효화 (스레드 모드용)
        with self._local_lock:
            self._local_cache.pop('subscribers_keys', None)
        print(f"[SignalBroker] unsubscribe: {signal_name} <- {task_id}")

    def _get_cached_subscribers(self) -> List[str]:
        """로컬 캐시에서 구독자 키 목록 가져오기 (IPC 최소화)"""
        # 프로세스 모드: 버전 체크로 캐시 유효성 확인
        if self._mode == 'process':
            remote_version = self._subscribers.get('__version__', 0)  # IPC (단일 값)
            with self._local_lock:
                local_version = self._local_cache.get('version', -1)
                if local_version == remote_version and 'subscribers_keys' in self._local_cache:
                    return self._local_cache['subscribers_keys']
            # 캐시 미스 또는 버전 불일치: Manager.dict()에서 읽기
            with self._lock:
                all_keys = [k for k in self._subscribers.keys() if k != '__version__']
            with self._local_lock:
                self._local_cache['subscribers_keys'] = all_keys
                self._local_cache['version'] = remote_version
            return all_keys
        else:
            # 스레드 모드: 단순 캐시
            with self._local_lock:
                cached = self._local_cache.get('subscribers_keys')
                if cached is not None:
                    return cached
            with self._lock:
                all_keys = list(self._subscribers.keys())
            with self._local_lock:
                self._local_cache['subscribers_keys'] = all_keys
            return all_keys

    def emit(self, signal: Signal, validate: bool = False) -> bool:
        """시그널 발행. validate=True면 pickle 검증."""
        if validate and self._mode == 'process' and not signal.validate_for_ipc():
            return False

        # 로컬 캐시 사용 (IPC 최소화)
        prefix = f"{signal.name}:"
        all_keys = self._get_cached_subscribers()
        subscribers = [k.split(':')[1] for k in all_keys if k.startswith(prefix)]

        if not subscribers:
            return False

        # 큐는 캐시하지 않음 (런타임에 변경될 수 있음)
        with self._lock:
            q_map = {tid: self._queues.get(tid) for tid in subscribers}

        now_str = datetime.now().strftime('%H:%M:%S.%f')[:-3]
        print(f"[SignalBroker] {now_str} emit '{signal.name}' to {subscribers}")
        data = signal.__dict__
        sent = False
        for tid, q in q_map.items():
            if q:
                try:
                    q.put(data, block=False)
                    sent = True
                except queue.Full:
                    print(f"[SignalBroker] Queue full for task '{tid}', signal '{signal.name}' dropped")
                except Exception as e:
                    print(f"[SignalBroker] emit error to '{tid}': {e}")
        # 통계 수집 (stats_enabled가 True인 경우)
        if sent:
            self._record_stats(signal.name)
        return sent

    def emit_to(self, task_id: str, signal: Signal, validate: bool = False) -> bool:
        """특정 태스크에 시그널 직접 발행."""
        if validate and self._mode == 'process' and not signal.validate_for_ipc():
            return False

        with self._lock:
            q = self._queues.get(task_id)
        if q:
            try:
                q.put(signal.__dict__, block=False)
                return True
            except queue.Full:
                print(f"[SignalBroker] Queue full for task '{task_id}', signal '{signal.name}' dropped")
            except Exception as e:
                print(f"[SignalBroker] emit_to error '{task_id}': {e}")
        return False

    def get_subscribers(self, signal_name: str) -> List[str]:
        with self._lock:
            lst = self._subscribers.get(signal_name)
            return lst[:] if lst else []

    # ─────────────────────────────────────────────────────────────────────────
    # Statistics Methods
    # ─────────────────────────────────────────────────────────────────────────

    def _record_stats(self, signal_name: str):
        """내부용: 시그널 통계 기록"""
        enabled = self._stats_enabled.value if self._mode == 'process' else self._stats_enabled
        if not enabled:
            return
        now = time.time()
        with self._lock:
            current = self._stats.get(signal_name, {'count': 0, 'last_time': 0.0})
            self._stats[signal_name] = {
                'count': current['count'] + 1,
                'last_time': now
            }

    @property
    def stats_enabled(self) -> bool:
        """통계 수집 활성화 여부"""
        return self._stats_enabled.value if self._mode == 'process' else self._stats_enabled

    @stats_enabled.setter
    def stats_enabled(self, value: bool):
        """통계 수집 on/off 설정"""
        if self._mode == 'process':
            self._stats_enabled.value = value
        else:
            self._stats_enabled = value

    def get_stats(self) -> Dict[str, Any]:
        """시그널 통계 조회. {enabled, cleared_at, signals: {name: {count, last_time}}}"""
        enabled = self._stats_enabled.value if self._mode == 'process' else self._stats_enabled
        cleared_at = self._stats_cleared_at.value if self._mode == 'process' else self._stats_cleared_at
        with self._lock:
            signals = dict(self._stats)
        return {
            'enabled': enabled,
            'cleared_at': cleared_at,
            'signals': signals
        }

    def clear_stats(self):
        """시그널 통계 초기화 (클리어 시간 기록)"""
        now = time.time()
        with self._lock:
            self._stats.clear()
        if self._mode == 'process':
            self._stats_cleared_at.value = now
        else:
            self._stats_cleared_at = now
        return now


class SignalClient:
    """시그널 클라이언트 (순환 호출 방지 포함)"""
    __slots__ = ('_broker', '_task_id', '_queue', '_handlers', '_running', '_thread', '_in_handler')

    def __init__(self, broker: SignalBroker, task_id: str):
        self._broker = broker
        self._task_id = task_id
        self._queue = broker.register(task_id)
        self._handlers: Dict[str, List[Callable]] = {}
        self._running = False
        self._thread = None
        self._in_handler: set = set()  # 현재 처리 중인 signal_name (순환 호출 방지)

    def start(self):
        self._running = True
        self._thread = threading.Thread(target=self._recv_loop, daemon=True)
        self._thread.start()

    def stop(self):
        self._running = False
        # 스레드 종료 대기 후 unregister
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=_STOP_TIMEOUT)
        self._broker.unregister(self._task_id)

    def _recv_loop(self):
        q, handlers, in_handler = self._queue, self._handlers, self._in_handler
        while self._running:
            try:
                data = q.get(timeout=_RECV_TIMEOUT)
                signal = Signal(**data) if isinstance(data, dict) else data

                # 순환 호출 방지: 이미 같은 signal_name 핸들러 실행 중이면 무시
                if signal.name in in_handler:
                    print(f"[SignalClient:{self._task_id}] Circular call blocked: '{signal.name}'")
                    continue

                lst = handlers.get(signal.name)
                if lst:
                    in_handler.add(signal.name)
                    try:
                        # 모든 핸들러 실행 (@rmi_signal + signal_bridge 모두)
                        for idx, handler in enumerate(lst):
                            try:
                                handler(signal)
                            except Exception as e:
                                tb = traceback.format_exc()
                                handler_name = getattr(handler, '__name__', f'handler_{idx}')
                                print(f"[SignalClient:{self._task_id}] Handler error '{handler_name}' for '{signal.name}':\n{tb}")
                    finally:
                        in_handler.discard(signal.name)
            except queue.Empty:
                continue
            except (BrokenPipeError, OSError, EOFError):
                # 파이프 종료 시 정상 종료
                break
            except Exception as e:
                # 종료 중이면 에러 무시
                if self._running:
                    print(f"[SignalClient:{self._task_id}] recv_loop error: {e}")
                else:
                    break

    def on(self, signal_name: str, handler: Callable[[Signal], None]):
        h = self._handlers
        if signal_name not in h:
            h[signal_name] = []
        h[signal_name].append(handler)
        self._broker.subscribe(signal_name, self._task_id)

    def off(self, signal_name: str, handler: Callable[[Signal], None] = None):
        lst = self._handlers.get(signal_name)
        if lst:
            if handler:
                if handler in lst:
                    lst.remove(handler)
            else:
                lst.clear()
        if not self._handlers.get(signal_name):
            self._broker.unsubscribe(signal_name, self._task_id)

    def off_all(self):
        """모든 시그널 구독 해제"""
        for signal_name in list(self._handlers.keys()):
            self._broker.unsubscribe(signal_name, self._task_id)
        self._handlers.clear()

    def emit(self, signal_name: str, data: Any = None):
        self._broker.emit(Signal(name=signal_name, source=self._task_id, data=data))

    def emit_to(self, task_id: str, signal_name: str, data: Any = None):
        self._broker.emit_to(task_id, Signal(name=signal_name, source=self._task_id, data=data))
