# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.6 - sm_signal                                                     ║
║  Signal Infrastructure — Registry & Statistics in Shared Memory             ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.6.0                                                             ║
║  Date    : 2026-03-04                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Classes:                                                                    ║
║    SmSignalRegistry — 전역 구독자 레지스트리 (비트맵 O(1) 조회)             ║
║    SmSignalStats    — 실시간 시그널 성능 통계소 (Latency/TPS)               ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

import time
import numpy as np
from ._base import cleanup_existing_shm, _ShmBase
from typing import Dict, List, Optional


class RegistryFullError(RuntimeError):
    """Registry 슬롯 고갈 — 태스크 또는 시그널 등록 한계 초과 시 발생

    SmSignalRegistry는 최대 64개 태스크, 256개 시그널을 지원합니다.
    """
    def __init__(self, resource: str, current: int, maximum: int, name: str):
        self.resource = resource
        self.current = current
        self.maximum = maximum
        self.name = name
        super().__init__(
            f"{resource} slot exhausted: {current}/{maximum}. "
            f"Cannot register '{name}'."
        )


# ═══════════════════════════════════════════════════════════════
#  SmSignalRegistry — Global Signal Subscription Bitmask
# ═══════════════════════════════════════════════════════════════

class SmSignalRegistry(_ShmBase):
    """공유 메모리 기반 시그널 레지스트리

    어떤 태스크가 어떤 시그널을 구독하고 있는지 비트맵(Bitmask)으로 관리하여
    시그널 발행 시 구독자 조회를 극도로 빠르게 수행합니다.
    최대 64개의 태스크와 256개의 시그널을 지원합니다.
    """
    __slots__ = ('_lock', '_header', '_task_table', '_signal_table',
                 '_sig_name_cache', '_task_id_cache')

    MAX_TASKS = 64
    MAX_SIGNALS = 256

    HEADER_SIZE = 64
    TASK_TABLE_SIZE = 64 * 64   # 64B * 64
    SIGNAL_TABLE_SIZE = 256 * 128 # 128B * 256

    # Task Slot (64B):
    # [0:32]   task_id (char[32])
    # [32:33]  status (uint8, 0=None, 1=Active, 2=Zombie)
    # [33:41]  last_alive (float64)
    # [41:49]  inbox_ptr (uint64, SmSignalBus 연동용)
    # [49:64]  reserved (15B)

    # Signal Slot (128B):
    # [0:64]   sig_name (char[64])
    # [64:72]  sub_mask (uint64, 각 비트는 Task 인덱스에 대응)
    # [72:80]  last_seq (int64)
    # [80:128] reserved (48B)

    def __init__(self, name: str = "alaska_sig_registry", create: bool = False, lock=None):
        self._lock = lock
        total_size = self.HEADER_SIZE + self.TASK_TABLE_SIZE + self.SIGNAL_TABLE_SIZE

        if create:
            cleanup_existing_shm(name)
        self._init_shm(name, total_size, create)
        if create:
            self._init_header()

        self._header = np.ndarray((1,), dtype=[
            ('max_signals', 'i4'),
            ('max_tasks', 'i4'),
            ('active_signals', 'i4'),
            ('active_tasks', 'i4'),
            ('version', 'i8'),
            ('reserved', 'u1', 40)
        ], buffer=self._shm.buf, offset=0)

        self._task_table = np.ndarray((self.MAX_TASKS,), dtype=[
            ('task_id', 'S32'),
            ('status', 'u1'),
            ('last_alive', 'f8'),
            ('inbox_ptr', 'u8'),
            ('reserved', 'u1', 15)
        ], buffer=self._shm.buf, offset=self.HEADER_SIZE)

        self._signal_table = np.ndarray((self.MAX_SIGNALS,), dtype=[
            ('sig_name', 'S64'),
            ('sub_mask', 'u8'),
            ('last_seq', 'i8'),
            ('reserved', 'u1', 48)
        ], buffer=self._shm.buf, offset=self.HEADER_SIZE + self.TASK_TABLE_SIZE)

        # 로컬 캐시
        self._sig_name_cache: Dict[str, int] = {}
        self._task_id_cache: Dict[str, int] = {}

    def _init_header(self):
        buf = np.ndarray((1,), dtype=[
            ('max_signals', 'i4'),
            ('max_tasks', 'i4'),
            ('active_signals', 'i4'),
            ('active_tasks', 'i4'),
            ('version', 'i8'),
            ('reserved', 'u1', 40)
        ], buffer=self._shm.buf, offset=0)
        buf['max_signals'] = self.MAX_SIGNALS
        buf['max_tasks'] = self.MAX_TASKS
        buf['active_signals'] = 0
        buf['active_tasks'] = 0
        buf['version'] = 0

    def register_task(self, task_id: str) -> int:
        """태스크 등록 및 인덱스 반환 (0~63).

        다중 프로세스에서 동시 호출 시 lock 필요 (RC-02).
        """
        if self._lock:
            with self._lock:
                return self._register_task(task_id)
        return self._register_task(task_id)

    def _register_task(self, task_id: str) -> int:
        """태스크 등록 내부 구현 (lock 미포함)."""
        if task_id in self._task_id_cache:
            return self._task_id_cache[task_id]

        tid_bytes = task_id.encode('ascii')[:31]
        active = int(self._header['active_tasks'][0])

        if active >= self.MAX_TASKS:
            raise RegistryFullError("Task", active, self.MAX_TASKS, task_id)

        for i in range(active):
            if self._task_table[i]['task_id'] == tid_bytes:
                self._task_id_cache[task_id] = i
                return i

        idx = active
        self._task_table[idx]['task_id'] = tid_bytes
        self._task_table[idx]['status'] = 1
        self._task_table[idx]['last_alive'] = float(time.time())
        self._header['active_tasks'][0] = int(active + 1)
        self._header['version'][0] = int(self._header['version'][0] + 1)
        self._task_id_cache[task_id] = idx
        return idx

    def subscribe(self, sig_name: str, task_id: str) -> bool:
        """시그널 구독 (비트 마스크 설정).

        다중 프로세스에서 동시 호출 시 lock 필요 (RC-02).
        lock 없으면 Read-OR-Write 경합으로 비트 손실 발생.
        """
        if self._lock:
            with self._lock:
                return self._subscribe(sig_name, task_id)
        return self._subscribe(sig_name, task_id)

    def _subscribe(self, sig_name: str, task_id: str) -> bool:
        """구독 내부 구현 (lock 미포함).

        Raises:
            RegistryFullError: 태스크 또는 시그널 슬롯 고갈 시
        """
        task_idx = self._register_task(task_id)
        sig_idx = self._find_sig_slot(sig_name)

        current_mask = int(self._signal_table[sig_idx]['sub_mask'])
        self._signal_table[sig_idx]['sub_mask'] = current_mask | (1 << task_idx)
        self._header['version'][0] += 1
        return True

    def unsubscribe(self, sig_name: str, task_id: str) -> bool:
        """시그널 구독 해제 (비트 마스크 해제).

        다중 프로세스에서 동시 호출 시 lock 필요 (RC-02).
        """
        if self._lock:
            with self._lock:
                return self._unsubscribe(sig_name, task_id)
        return self._unsubscribe(sig_name, task_id)

    def _unsubscribe(self, sig_name: str, task_id: str) -> bool:
        """구독 해제 내부 구현 (lock 미포함).

        Raises:
            RegistryFullError: 태스크 또는 시그널 슬롯 고갈 시
        """
        task_idx = self._register_task(task_id)
        sig_idx = self._find_sig_slot(sig_name)

        current_mask = int(self._signal_table[sig_idx]['sub_mask'])
        self._signal_table[sig_idx]['sub_mask'] = current_mask & ~(1 << task_idx)
        self._header['version'][0] += 1
        return True

    def get_subscribers(self, sig_name: str) -> List[str]:
        """시그널의 모든 구독 태스크 ID 반환"""
        sig_idx = self._find_sig_slot(sig_name, create=False)
        if sig_idx == -1:
            return []

        mask = int(self._signal_table[sig_idx]['sub_mask'])
        if mask == 0:
            return []

        subscribers = []
        remaining = mask
        i = 0
        while remaining:
            if remaining & 1:
                tid = self._task_table[i]['task_id'].decode('ascii').rstrip('\x00')
                if tid:
                    subscribers.append(tid)
            remaining >>= 1
            i += 1
        return subscribers

    def _find_sig_slot(self, sig_name: str, create: bool = True) -> int:
        if sig_name in self._sig_name_cache:
            return self._sig_name_cache[sig_name]

        sig_bytes = sig_name.encode('ascii')[:63]
        active = self._header['active_signals'][0]

        for i in range(active):
            if self._signal_table[i]['sig_name'] == sig_bytes:
                self._sig_name_cache[sig_name] = i
                return i

        if not create:
            return -1
        if active >= self.MAX_SIGNALS:
            raise RegistryFullError("Signal", int(active), self.MAX_SIGNALS, sig_name)

        idx = int(active)
        self._signal_table[idx]['sig_name'] = sig_bytes
        self._signal_table[idx]['sub_mask'] = 0
        self._header['active_signals'][0] = int(active + 1)
        self._header['version'][0] = int(self._header['version'][0] + 1)
        self._sig_name_cache[sig_name] = idx
        return idx

    def update_heartbeat(self, task_id: str):
        """태스크 생존 신고"""
        idx = self.register_task(task_id)
        self._task_table[idx]['last_alive'] = time.time()

    def get_version(self) -> int:
        """레지스트리 변경 버전 조회 (고속 캐시 무효화용)"""
        return int(self._header['version'][0])

    def __repr__(self):
        return (f"SmSignalRegistry(v{self.get_version()}, "
                f"tasks={self._header['active_tasks'][0]}, "
                f"sigs={self._header['active_signals'][0]})")


# ═══════════════════════════════════════════════════════════════
#  SmSignalStats — Real-time Signal Performance Statistics
# ═══════════════════════════════════════════════════════════════

class SmSignalStats(_ShmBase):
    """공유 메모리 기반 실시간 시그널 통계소

    Manager.dict()의 오버헤드 없이 수백 개의 시그널 통계를 1μs 이내에 기록하고
    웹 디버거 등에서 즉시 조회할 수 있습니다.
    """
    __slots__ = ('_max_slots', '_lock', '_header', '_slots', '_name_cache')

    # 슬롯 구조: 128 bytes
    # [0:64]    name (char[64])
    # [64:72]   count (int64)
    # [72:80]   last_ts (float64)
    # [80:84]   transit_min (float32)
    # [84:88]   transit_avg (float32)
    # [88:92]   transit_max (float32)
    # [92:96]   proc_min (float32)
    # [96:100]  proc_avg (float32)
    # [100:104] proc_max (float32)
    # [104:108] qlen (int32)
    # [108:112] miss_count (int32)
    # [112:128] reserved (16 bytes)

    SLOT_SIZE = 128
    HEADER_SIZE = 32

    def __init__(self, name: str = "alaska_sig_stats", max_slots: int = 256,
                 create: bool = False, lock=None):
        self._max_slots = max_slots
        self._lock = lock
        total_size = self.HEADER_SIZE + (self.SLOT_SIZE * max_slots)

        if create:
            cleanup_existing_shm(name)
        self._init_shm(name, total_size, create)
        if create:
            self._init_header()

        self._header = np.ndarray((1,), dtype=[
            ('max_slots', 'i4'),
            ('active_slots', 'i4'),
            ('cleared_at', 'f8'),
            ('reserved', 'u1', 16)
        ], buffer=self._shm.buf, offset=0)

        self._slots = np.ndarray((max_slots,), dtype=[
            ('name', 'S64'),
            ('count', 'i8'),
            ('last_ts', 'f8'),
            ('transit_min', 'f4'),
            ('transit_avg', 'f4'),
            ('transit_max', 'f4'),
            ('proc_min', 'f4'),
            ('proc_avg', 'f4'),
            ('proc_max', 'f4'),
            ('qlen', 'i4'),
            ('miss_count', 'i4'),
            ('reserved', 'u1', 16)
        ], buffer=self._shm.buf, offset=self.HEADER_SIZE)

        self._name_cache: Dict[str, int] = {}

    def _init_header(self):
        buf = np.ndarray((1,), dtype=[
            ('max_slots', 'i4'),
            ('active_slots', 'i4'),
            ('cleared_at', 'f8'),
            ('reserved', 'u1', 16)
        ], buffer=self._shm.buf, offset=0)
        buf['max_slots'] = self._max_slots
        buf['active_slots'] = 0
        buf['cleared_at'] = time.time()

    def _find_slot(self, sig_name: str) -> int:
        """시그널명에 해당하는 슬롯 인덱스 찾기 (없으면 새로 할당)"""
        if sig_name in self._name_cache:
            return self._name_cache[sig_name]

        active = self._header['active_slots'][0]
        sig_bytes = sig_name.encode('ascii')[:63]

        for i in range(active):
            if self._slots[i]['name'] == sig_bytes:
                self._name_cache[sig_name] = i
                return i

        if active >= self._max_slots:
            print(f"[SmSignalStats] ERROR: Maximum slots ({self._max_slots}) reached. Cannot record '{sig_name}'.")
            return -1

        if self._lock:
            with self._lock:
                idx = self._header['active_slots'][0]
                if idx < self._max_slots:
                    self._slots[idx]['name'] = sig_bytes
                    self._slots[idx]['count'] = 0
                    self._header['active_slots'][0] += 1
                    self._name_cache[sig_name] = idx
                    return idx
        else:
            idx = active
            self._slots[idx]['name'] = sig_bytes
            self._slots[idx]['count'] = 0
            self._header['active_slots'][0] = int(active + 1)
            self._name_cache[sig_name] = idx
            return idx

        return -1

    def record(self, sig_name: str, transit_ms: float, proc_ms: float, qlen: int = 0):
        """시그널 타이밍 기록"""
        idx = self._find_slot(sig_name)
        if idx == -1:
            return

        slot = self._slots[idx]
        count = int(slot['count'])

        slot['count'] = count + 1
        slot['last_ts'] = float(time.time())

        if count == 0:
            slot['transit_min'] = float(transit_ms)
            slot['transit_avg'] = float(transit_ms)
            slot['transit_max'] = float(transit_ms)
            slot['proc_min'] = float(proc_ms)
            slot['proc_avg'] = float(proc_ms)
            slot['proc_max'] = float(proc_ms)
        else:
            alpha = 0.1
            slot['transit_min'] = float(min(slot['transit_min'], transit_ms))
            slot['transit_max'] = float(max(slot['transit_max'], transit_ms))
            slot['transit_avg'] = float((1-alpha) * slot['transit_avg'] + alpha * transit_ms)

            slot['proc_min'] = float(min(slot['proc_min'], proc_ms))
            slot['proc_max'] = float(max(slot['proc_max'], proc_ms))
            slot['proc_avg'] = float((1-alpha) * slot['proc_avg'] + alpha * proc_ms)

        slot['qlen'] = int(qlen)

    def record_miss(self, sig_name: str):
        """데드라인 위반 기록"""
        idx = self._find_slot(sig_name)
        if idx != -1:
            self._slots[idx]['miss_count'] = int(self._slots[idx]['miss_count'] + 1)

    def get_stats(self, sig_name: str) -> Optional[Dict]:
        """특정 시그널의 통계 조회"""
        idx = self._find_slot(sig_name)
        if idx == -1:
            return None

        slot = self._slots[idx]
        return {
            'name': slot['name'].decode('ascii').rstrip('\x00'),
            'count': int(slot['count']),
            'last_ts': float(slot['last_ts']),
            'transit': {
                'min': float(slot['transit_min']),
                'avg': float(slot['transit_avg']),
                'max': float(slot['transit_max'])
            },
            'proc': {
                'min': float(slot['proc_min']),
                'avg': float(slot['proc_avg']),
                'max': float(slot['proc_max'])
            },
            'qlen': int(slot['qlen']),
            'miss_count': int(slot['miss_count'])
        }

    def get_all(self) -> Dict[str, Dict]:
        """전체 시그널 통계 조회"""
        active = int(self._header['active_slots'][0])
        results = {}
        for i in range(active):
            slot = self._slots[i]
            name = slot['name'].decode('ascii').rstrip('\x00')
            results[name] = {
                'count': int(slot['count']),
                'last_ts': float(slot['last_ts']),
                'transit_avg': float(slot['transit_avg']),
                'proc_avg': float(slot['proc_avg']),
                'qlen': int(slot['qlen']),
                'miss_count': int(slot['miss_count'])
            }
        return results

    def reset(self):
        """모든 통계 초기화"""
        self._header['active_slots'] = 0
        self._header['cleared_at'] = time.time()
        self._name_cache.clear()

    def __repr__(self):
        return f"SmSignalStats({self._name}, slots={self._header['active_slots'][0]}/{self._max_slots})"
