# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.3 - SmBlackBox                                                    ║
║  Signal Chain Session Monitoring and Completion Assurance in Shared Memory   ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.3.0                                                             ║
║  Date    : 2026-02-28                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    복합 시그널 체인의 실행 완료를 보장하고,                                  ║
║    미완료 시(타임아웃) 경보를 발생시키는 세션 감시 시스템                    ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

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


# Session status
BB_FREE = 0
BB_ACTIVE = 1
BB_COMPLETED = 2
BB_ALARM = 3

# Event types
BB_EVENT_EMIT = 1
BB_EVENT_IPC_CALL = 2
BB_EVENT_HANDLE_START = 3
BB_EVENT_HANDLE_END = 4
BB_EVENT_IPC_RESPONSE = 5
BB_EVENT_ACK = 6


class SmBlackBox(_ShmBase):
    """공유 메모리 기반 BlackBox 세션 관리자

    비즈니스 레벨의 시그널 체인이 계획대로 완료되었는지 실시간 감시합니다.
    최대 64개의 동시 세션을 지원하며, 각 세션은 최근 16개의 이벤트 로그를 유지합니다.
    """
    __slots__ = ('_lock', '_slots', '_bb_cache')

    MAX_SESSIONS = 64
    SLOT_SIZE = 512
    EVENT_SIZE = 32 # 32B * 8 events = 256B for log
    MAX_LOG_EVENTS = 8

    # Session Slot (512B):
    # [0:1]    status (uint8, 0=Free, 1=Active, 2=Completed, 3=Alarm)
    # [1:65]   bb_id (char[64])
    # [65:97]  source_task (char[32])
    # [97:105] created_at (float64)
    # [105:109] timeout (float32)
    # [109:111] expected_steps (int16)
    # [111:113] done_steps (int16)
    # [113:121] completed_at (float64)
    # [121:377] event_log (32B * 8 = 256B)
    # [377:512] reserved (135B)

    # Event Record (32B):
    # [0:8]    timestamp (float64)
    # [8:9]    event_type (uint8)
    # [9:10]   depth (uint8)
    # [10:20]  task_id (char[10] - hash or prefix)
    # [20:30]  sig_name (char[10] - hash or prefix)
    # [30:32]  reserved (2B)

    def __init__(self, name: str = "alaska_blackbox", create: bool = False, lock=None):
        self._lock = lock
        total_size = self.MAX_SESSIONS * self.SLOT_SIZE

        if create:
            cleanup_existing_shm(name)
        self._init_shm(name, total_size, create)
            
        self._slots = np.ndarray((self.MAX_SESSIONS,), dtype=[
            ('status', 'u1'),
            ('bb_id', 'S64'),
            ('source_task', 'S32'),
            ('created_at', 'f8'),
            ('timeout', 'f4'),
            ('expected_steps', 'i2'),
            ('done_steps', 'i2'),
            ('completed_at', 'f8'),
            ('log_head', 'i4'), # 링버퍼 헤드
            ('event_log', 'u1', self.EVENT_SIZE * self.MAX_LOG_EVENTS),
            ('reserved', 'u1', 131)
        ], buffer=self._shm.buf)

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

    def open_session(self, bb_id: str, source_task: str, expected_steps: int = 0, 
                     timeout: float = 5.0) -> int:
        """새 BlackBox 세션 생성"""
        bb_bytes = bb_id.encode('ascii')[:63]
        
        # 빈 슬롯 찾기
        idx = -1
        for i in range(self.MAX_SESSIONS):
            if self._slots[i]['status'] == BB_FREE:
                idx = i
                break
        
        if idx == -1:
            return -1
            
        slot = self._slots[idx]
        slot['status'] = BB_ACTIVE
        slot['bb_id'] = bb_bytes
        slot['source_task'] = source_task.encode('ascii')[:31]
        slot['created_at'] = time.time()
        slot['timeout'] = timeout
        slot['expected_steps'] = expected_steps
        slot['done_steps'] = 0
        slot['log_head'] = 0
        # 로그 영역 초기화
        slot['event_log'][:] = 0
        
        self._bb_cache[bb_id] = idx
        return idx

    def record_event(self, bb_id: str, event_type: int, task_id: str, 
                     sig_name: str, depth: int = 0):
        """세션에 이벤트 기록"""
        idx = self._get_bb_idx(bb_id)
        if idx == -1:
            return
            
        slot = self._slots[idx]
        if slot['status'] != BB_ACTIVE:
            return
            
        # HANDLE_END 시 스텝 증가
        if event_type == BB_EVENT_HANDLE_END:
            slot['done_steps'] += 1
            if slot['expected_steps'] > 0 and slot['done_steps'] >= slot['expected_steps']:
                # 자동 완료 처리 (명시적 ACK 권장)
                pass

        # 링버퍼 로그 기록
        head = slot['log_head']
        log_buf = slot['event_log'].reshape((self.MAX_LOG_EVENTS, self.EVENT_SIZE))
        
        event_entry = np.ndarray((1,), dtype=[
            ('timestamp', 'f8'),
            ('event_type', 'u1'),
            ('depth', 'u1'),
            ('task_id', 'S10'),
            ('sig_name', 'S10'),
            ('reserved', 'u1', 2)
        ], buffer=log_buf[head])
        
        event_entry['timestamp'] = time.time()
        event_entry['event_type'] = event_type
        event_entry['depth'] = depth
        event_entry['task_id'] = task_id.encode('ascii')[:9]
        event_entry['sig_name'] = sig_name.encode('ascii')[:9]
        
        slot['log_head'] = (head + 1) % self.MAX_LOG_EVENTS

    def acknowledge(self, bb_id: str) -> bool:
        """세션 완료 확인 (ACK)"""
        idx = self._get_bb_idx(bb_id)
        if idx == -1:
            return False
            
        slot = self._slots[idx]
        if slot['status'] == BB_ACTIVE:
            slot['status'] = BB_COMPLETED
            slot['completed_at'] = time.time()
            return True
        return False

    def scan_alarms(self) -> List[Dict]:
        """타임아웃 초과 세션 스캔 및 상태 변경"""
        now = time.time()
        alarms = []
        for i in range(self.MAX_SESSIONS):
            slot = self._slots[i]
            if slot['status'] == BB_ACTIVE:
                elapsed = now - slot['created_at']
                if elapsed > slot['timeout']:
                    slot['status'] = BB_ALARM
                    bb_id = slot['bb_id'].decode('ascii').rstrip('\x00')
                    alarms.append({
                        'bb_id': bb_id,
                        'source': slot['source_task'].decode('ascii').rstrip('\x00'),
                        'done': int(slot['done_steps']),
                        'expected': int(slot['expected_steps']),
                        'elapsed': float(elapsed)
                    })
        return alarms

    def cleanup(self, ttl: float = 1.0):
        """완료/알람 세션 정리 (1초 후 삭제)"""
        now = time.time()
        for i in range(self.MAX_SESSIONS):
            slot = self._slots[i]
            if slot['status'] in (BB_COMPLETED, BB_ALARM):
                ts = slot['completed_at'] if slot['status'] == BB_COMPLETED else slot['created_at'] + slot['timeout']
                if now - ts > ttl:
                    slot['status'] = BB_FREE
                    bb_id = slot['bb_id'].decode('ascii').rstrip('\x00')
                    if bb_id in self._bb_cache:
                        del self._bb_cache[bb_id]

    def _get_bb_idx(self, bb_id: str) -> int:
        if bb_id in self._bb_cache:
            return self._bb_cache[bb_id]
        
        bb_bytes = bb_id.encode('ascii')[:63]
        for i in range(self.MAX_SESSIONS):
            slot = self._slots[i]
            if slot['status'] != BB_FREE and slot['bb_id'] == bb_bytes:
                self._bb_cache[bb_id] = i
                return i
        return -1

    # close() → _ShmBase.close() 상속 (중복 호출 방지 포함)

    def __repr__(self):
        active = sum(1 for i in range(self.MAX_SESSIONS) if self._slots[i]['status'] != BB_FREE)
        return f"SmBlackBox(active={active}/{self.MAX_SESSIONS})"
