# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.3 - SmSignalStats                                                 ║
║  Real-time Signal Performance Statistics in Shared Memory                    ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.3.0                                                             ║
║  Date    : 2026-02-28                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    모든 시그널의 성능 지표(Latency, TPS)를 프로세스 간 부하 없이             ║
║    공유 메모리에 실시간 집계하는 컴포넌트                                     ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

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


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

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

    # 슬롯 구조: 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
    
    # Header: [0:4] max_slots, [4:8] active_slots, [8:16] cleared_at, [16:32] reserved

    def __init__(self, name: str = "alaska_sig_stats", max_slots: int = 256, 
                 create: bool = False, lock=None):
        self._name = name
        self._max_slots = max_slots
        self._is_new = create
        self._lock = lock
        
        total_size = self.HEADER_SIZE + (self.SLOT_SIZE * max_slots)
        
        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_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
            
        # Lock 하에서 할당 (필요 시)
        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]
        # 설계 9.4: 표준 Python 타입으로 명시적 형변환 (JSON 직렬화 및 정합성 보장)
        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 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"SmSignalStats({self._name}, slots={self._header['active_slots'][0]}/{self._max_slots})"
