# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.3 - SmSignalRegistry                                              ║
║  Global Service Registry and Subscription Bitmask in Shared Memory           ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.3.0                                                             ║
║  Date    : 2026-02-28                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    시그널 구독 정보를 공유 메모리로 관리하여,                                ║
║    emit() 시점에 구독 태스크를 O(1)로 식별하는 고속 레지스트리               ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

import time
import numpy as np
from multiprocessing.shared_memory import SharedMemory
from typing import Dict, List, Optional


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

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

    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._name = name
        self._is_new = create
        self._lock = lock
        
        total_size = self.HEADER_SIZE + self.TASK_TABLE_SIZE + self.SIGNAL_TABLE_SIZE
        
        if create:
            try:
                SharedMemory(name=name).unlink()
            except Exception:
                pass
            self._shm = SharedMemory(name=name, create=True, size=total_size)
            self._init_header()
        else:
            self._shm = SharedMemory(name=name, create=False)
            
        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])

        # 설계 9.2: 슬롯 임계값 보호 (Boundary Protection)
        if active >= self.MAX_TASKS:
            print(f"[SmSignalRegistry] CRITICAL: Task limit reached ({self.MAX_TASKS}). Cannot register '{task_id}'.")
            return -1

        # 검색 (중복 등록 방지)
        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 미포함)."""
        task_idx = self._register_task(task_id)
        if task_idx == -1:
            return False

        sig_idx = self._find_sig_slot(sig_name)
        if sig_idx == -1:
            return False

        # 비트 설정 (mask |= 1 << task_idx)
        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 미포함)."""
        task_idx = self._register_task(task_id)
        if task_idx == -1:
            return False

        sig_idx = self._find_sig_slot(sig_name)
        if sig_idx == -1:
            return False

        # 비트 해제 (mask &= ~(1 << task_idx))
        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 = self._signal_table[sig_idx]['sub_mask']
        if mask == 0:
            return []
            
        subscribers = []
        for i in range(self.MAX_TASKS):
            if mask & (1 << i):
                tid = self._task_table[i]['task_id'].decode('ascii').rstrip('\x00')
                if tid:
                    subscribers.append(tid)
        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 or active >= self.MAX_SIGNALS:
            if create and active >= self.MAX_SIGNALS:
                print(f"[SmSignalRegistry] ERROR: Maximum signals ({self.MAX_SIGNALS}) reached. Cannot register '{sig_name}'.")
            return -1
            
        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)
        if idx != -1:
            self._task_table[idx]['last_alive'] = time.time()

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

    def close(self):
        self._shm.close()
        if self._is_new:
            try:
                self._shm.unlink()
            except FileNotFoundError:
                pass
            except PermissionError:
                import warnings; warnings.warn(f"SHM unlink failed (in use): {self._shm.name}", ResourceWarning, stacklevel=2)

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