"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASK v2.0 Project                                                          ║
║  Task Signal - Process-Safe Signal Communication Module                      ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.0.0                                                             ║
║  Date    : 2026-01-30                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Classes:                                                                    ║
║    - Signal       : Dataclass representing a signal message                  ║
║    - SignalBroker : Central broker managing queues and subscriptions         ║
║    - SignalClient : Client wrapper for signal emission and reception         ║
║                                                                              ║
║  Functions:                                                                  ║
║    - _is_picklable : Check if an object can be pickled for IPC               ║
║                                                                              ║
║  Notes (Thread/Process Mixed Mode):                                          ║
║    1. Handlers cannot be passed between processes - register locally         ║
║    2. Data must be picklable (no lambdas, sockets, file handles)             ║
║    3. Process mode requires SyncManager                                      ║
║    4. Thread-only mode uses mgr=None for lightweight operation               ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

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

if TYPE_CHECKING:
    from multiprocessing.managers import SyncManager


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:
    """시그널 브로커 - Thread/Process 혼용 지원

    mgr=None: 스레드 전용 모드 (경량, 빠름)
    mgr=Manager(): 프로세스 간 통신 모드 (IPC)
    """
    __slots__ = ('_mgr', '_queues', '_subscribers', '_lock', '_mode')

    def __init__(self, mgr: Optional[SyncManager] = None):
        self._mgr = mgr
        self._mode = 'process' if mgr else 'thread'
        self._queues: Dict[str, Any] = {}
        self._subscribers: Dict[str, List[str]] = {}
        self._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)
            for subs in self._subscribers.values():
                if task_id in subs:
                    subs.remove(task_id)

    def subscribe(self, signal_name: str, task_id: str):
        subs = self._subscribers
        with self._lock:
            if signal_name not in subs:
                subs[signal_name] = []
            lst = subs[signal_name]
            if task_id not in lst:
                lst.append(task_id)

    def unsubscribe(self, signal_name: str, task_id: str):
        with self._lock:
            lst = self._subscribers.get(signal_name)
            if lst and task_id in lst:
                lst.remove(task_id)

    def emit(self, signal: Signal, validate: bool = False) -> bool:
        """시그널 발행

        Args:
            signal: 발행할 시그널
            validate: True이면 IPC 모드에서 pickle 가능 여부 검증

        Returns:
            성공 여부
        """
        if validate and self._mode == 'process' and not signal.validate_for_ipc():
            return False

        with self._lock:
            lst = self._subscribers.get(signal.name)
            subscribers = lst[:] if lst else []
            queues = self._queues
            q_map = {tid: queues.get(tid) for tid in 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}")
        return sent

    def emit_to(self, task_id: str, signal: Signal, validate: bool = False) -> bool:
        """특정 태스크에 시그널 발행

        Args:
            task_id: 대상 태스크 ID
            signal: 발행할 시그널
            validate: True이면 IPC 모드에서 pickle 가능 여부 검증

        Returns:
            성공 여부
        """
        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 []


class SignalClient:
    __slots__ = ('_broker', '_task_id', '_queue', '_handlers', '_running', '_thread')

    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

    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
        self._broker.unregister(self._task_id)

    def _recv_loop(self):
        q, handlers = self._queue, self._handlers
        while self._running:
            try:
                data = q.get(timeout=0.5)
                signal = Signal(**data) if isinstance(data, dict) else data
                lst = handlers.get(signal.name)
                if lst:
                    for handler in lst:
                        try:
                            handler(signal)
                        except Exception as e:
                            tb = traceback.format_exc()
                            print(f"[SignalClient:{self._task_id}] Handler error for '{signal.name}':\n{tb}")
            except queue.Empty:
                continue
            except Exception as e:
                print(f"[SignalClient:{self._task_id}] recv_loop error: {e}")

    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 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))
