# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""Signal 핵심 데이터 구조 — QoS, Signal, _SmSignalQueue, PriorityScheduler

task_signal.py에서 분리: 상수 + 데이터 클래스 + 큐 래퍼 + 스케줄러.
SignalBroker/SignalClient는 task_signal.py에 유지.
"""

from __future__ import annotations
from typing import Any
from dataclasses import dataclass, field
import struct
import pickle
import queue

from ..sm_infra.sm_ring_buffer import SmRingBuffer
from ..sm_infra.sm_sync import SmMutex as _WinMutex
from ._comm_utils import _IS_WIN32

_STOP_TIMEOUT = 1.0
_SIGNAL_QUEUE_SIZE = 1024
_PRIORITY_LEVELS = 5
_QUOTAS = [20, 10, 5, 2, 1]
_NOTIFY_WAIT_TIMEOUT = 0.1  # 커널 이벤트 대기 시간 (초)
_BP_RETRIES = 50            # Back Pressure: 큐 full 시 재시도 횟수 (~5ms)


class PayloadTooLargeError(Exception): pass


class QueueFullError(Exception):
    """Signal 큐 포화 — Backpressure (C5: 손실보다 예외)

    emit() 재시도(_BP_RETRIES) 소진 후 큐에 공간이 없으면 발생합니다.
    호출자가 이 예외를 받으면 발신 속도를 줄이거나 재시도해야 합니다.
    """
    def __init__(self, sig_name: str, failed_tids: list):
        self.sig_name = sig_name
        self.failed_tids = failed_tids
        super().__init__(
            f"Signal '{sig_name}' queue full for {len(failed_tids)} subscriber(s): "
            f"{', '.join(failed_tids)}. Retries({_BP_RETRIES}) exhausted."
        )


class QoS:
    CRITICAL   = {"priority": 1, "deadline": 0.010, "on_violation": "HALT"}
    REALTIME   = {"priority": 2, "deadline": 0.030, "on_violation": "LOG"}
    NORMAL     = {"priority": 3, "deadline": 0.100, "on_violation": "LOG"}
    LOW        = {"priority": 4, "deadline": 0.500, "on_violation": "IGNORE"}
    BACKGROUND = {"priority": 5, "deadline": 2.000, "on_violation": "IGNORE"}

    @staticmethod
    def from_input(val: Any) -> dict:
        if isinstance(val, dict): return val
        if isinstance(val, str): return getattr(QoS, val, QoS.NORMAL)
        return QoS.NORMAL


# ──────────────────────────────────────────────────────────────────────────────
# M-03: Signal struct encoding — 메타데이터 struct, data만 pickle
# ──────────────────────────────────────────────────────────────────────────────
_SIG_MAGIC = 0xA3
_SIG_HDR = struct.Struct('<BHHdB')  # 14B: magic(1) seq(2) depth(2) ts(8) qos_pri(1)
_QOS_BY_PRI = {1: QoS.CRITICAL, 2: QoS.REALTIME, 3: QoS.NORMAL,
               4: QoS.LOW, 5: QoS.BACKGROUND}


def _sig_encode(payload: dict) -> bytes:
    """Signal payload → binary (M-03: struct + pickle hybrid)

    고정 필드(name, source, seq, depth, ts, qos)는 struct.pack,
    가변 필드(data)만 pickle. data=None이면 pickle 완전 제거.
    """
    name_b = payload['name'].encode()
    source_b = payload.get('source', '').encode()
    nlen, slen = len(name_b), len(source_b)
    if nlen > 255 or slen > 255:
        return pickle.dumps(payload, protocol=pickle.HIGHEST_PROTOCOL)
    seq = payload.get('_seq', 0) & 0xFFFF
    depth = payload.get('_depth', 0) & 0xFFFF
    ts = payload.get('ts', 0.0)
    qos_pri = payload.get('qos', {}).get('priority', 3)
    data = payload.get('data')
    hdr = _SIG_HDR.pack(_SIG_MAGIC, seq, depth, ts, qos_pri)
    if data is None:
        return hdr + bytes([nlen]) + name_b + bytes([slen]) + source_b
    data_pkl = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
    return hdr + bytes([nlen]) + name_b + bytes([slen]) + source_b + data_pkl


def _sig_decode(raw: bytes) -> Any:
    """Binary → Signal payload dict (M-03: auto-detect format)

    magic 0xA3 → struct decode, 그 외 → pickle fallback (batch/old format).
    """
    if not raw or raw[0] != _SIG_MAGIC:
        return pickle.loads(raw)  # batch (list) or old pickle format
    _, seq, depth, ts, qos_pri = _SIG_HDR.unpack_from(raw, 0)
    off = _SIG_HDR.size
    nlen = raw[off]; off += 1
    name = raw[off:off + nlen].decode(); off += nlen
    slen = raw[off]; off += 1
    source = raw[off:off + slen].decode(); off += slen
    data = pickle.loads(raw[off:]) if off < len(raw) else None
    qos = _QOS_BY_PRI.get(qos_pri, {'priority': qos_pri})
    return {
        'name': name, 'source': source, 'data': data,
        'qos': qos, 'ts': ts, '_seq': seq, '_depth': depth,
    }


class _SmSignalQueue:
    """SmRingBuffer + Named Mutex wrapper — queue.Queue 호환 Signal IPC.

    MPSC(다중생산자-단일소비자) 패턴:
      - write: Named Mutex 보호 (다중 emitter, 커널 직접 호출)
      - read: 단일 recv thread (Lock 불필요)
    """
    __slots__ = ('_rb', '_lock', '_name', '_size', '_lock_name')

    def __init__(self, name, size=65536, create=False, lock_name=None):
        self._name = name
        self._size = size
        self._lock_name = lock_name
        if lock_name and _IS_WIN32:
            try:
                self._lock = _WinMutex(lock_name, create=create)
            except OSError:
                self._lock = _WinMutex(lock_name, create=False)
        else:
            self._lock = None
        if create:
            try:
                self._rb = SmRingBuffer(name, size=size, create=True)
            except FileExistsError:
                self._rb = SmRingBuffer(name, size=size, create=False)
                self._rb._is_new = True
                self._rb.reset()
        else:
            self._rb = SmRingBuffer(name, size=size, create=False)

    def put_nowait(self, obj):
        # M-03: Signal dict → struct encoding, 그 외 → pickle
        if isinstance(obj, dict) and 'name' in obj:
            data = _sig_encode(obj)
        else:
            data = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
        # F4: payload 크기 검사 (buffer capacity = size - 5)
        if len(data) + 4 > self._size - 1:
            raise PayloadTooLargeError(
                f"Signal payload {len(data)}B exceeds buffer capacity {self._size - 5}B")
        lock = self._lock
        if lock:
            with lock:
                if not self._rb.write(data):
                    raise queue.Full()
        else:
            if not self._rb.write(data):
                raise queue.Full()

    def write_raw(self, data: bytes):
        """Pre-pickled 바이트 직접 쓰기 — Broadcast Pre-pickle용 (v2.6)"""
        # F4: payload 크기 검사
        if len(data) + 4 > self._size - 1:
            raise PayloadTooLargeError(
                f"Signal payload {len(data)}B exceeds buffer capacity {self._size - 5}B")
        lock = self._lock
        if lock:
            with lock:
                if not self._rb.write(data):
                    raise queue.Full()
        else:
            if not self._rb.write(data):
                raise queue.Full()

    def get_nowait(self):
        data = self._rb.read()
        if data is None:
            raise queue.Empty()
        return _sig_decode(data)  # M-03: struct auto-detect

    def get(self, *a, **kw):
        return self.get_nowait()

    def empty(self):
        return self._rb._used() == 0

    def qsize(self):
        return self._rb._used()

    def close(self):
        if self._lock:
            try: self._lock.close()
            except Exception: pass
        self._rb.close()

    def reset(self):
        self._rb.reset()

    def __getstate__(self):
        return {'name': self._name, 'size': self._size, 'lock_name': self._lock_name}

    def __setstate__(self, state):
        self._name = state['name']
        self._size = state['size']
        self._lock_name = state.get('lock_name')
        self._rb = SmRingBuffer(self._name, self._size, create=False)
        if self._lock_name and _IS_WIN32:
            try:
                self._lock = _WinMutex(self._lock_name, create=False)
            except Exception:
                self._lock = None
        else:
            self._lock = None


class _QueueCompat(tuple):
    """v2.0 호환 래퍼: tuple[Queue] + 단일큐 API (NORMAL=P3으로 위임)"""
    def empty(self): return self[2].empty()
    def get(self, *a, **kw): return self[2].get(*a, **kw)
    def get_nowait(self): return self[2].get_nowait()
    def put(self, *a, **kw): return self[2].put(*a, **kw)
    def put_nowait(self, *a, **kw): return self[2].put_nowait(*a, **kw)
    def qsize(self): return self[2].qsize()


@dataclass
class Signal:
    name: str
    source: str
    data: Any = None
    qos: dict = field(default_factory=dict)
    _seq: int = 0
    _depth: int = 0
    signal_id: str = ""
    timestamp: float = 0.0


# ──────────────────────────────────────────────────────────────────────────────
# PriorityScheduler - Dual-Loop Shared Scheduler
# ──────────────────────────────────────────────────────────────────────────────
class PriorityScheduler:
    """듀얼 루프(RMI handler + Signal handler) 간 공유 우선순위 스케줄러.

    Weighted Fair Queuing + Starvation Guard 알고리즘:
      - 매 사이클 P1~P5 예산(budget) 리필
      - P1부터 순차 소비, 예산 소진 시 다음 우선순위로
      - N 사이클 연속 미처리 시 기아 방지 (강제 1회 처리)
    """
    __slots__ = ('_budgets', '_starvation', '_sig_count')

    CYCLE_QUOTAS = _QUOTAS                    # [20, 10, 5, 2, 1]
    STARVATION_LIMIT = 50

    def __init__(self):
        self._budgets = list(self.CYCLE_QUOTAS)
        self._starvation = [0] * _PRIORITY_LEVELS
        self._sig_count = 0

    def new_cycle(self):
        """매 폴링 사이클 시작 시 예산 리필."""
        self._budgets[:] = self.CYCLE_QUOTAS

    def consume(self, priority: int):
        """메시지 1건 처리 후 예산 차감."""
        if 0 <= priority < _PRIORITY_LEVELS:
            self._budgets[priority] = max(0, self._budgets[priority] - 1)
            self._starvation[priority] = 0
        self._sig_count += 1

    @property
    def stats(self) -> dict:
        return {'signal': self._sig_count, 'budgets': list(self._budgets)}
