# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.0 - Task Manager                                                  ║
║  Multiprocess/Thread Task Lifecycle & RMI Management                         ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.1.0                                                             ║
║  Date    : 2026-02-11                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    Task 생명주기 관리 및 RMI(Remote Method Invocation) 통신 시스템           ║
║    - Process/Thread 모드 Task 실행                                           ║
║    - IPC Queue 기반 원격 메서드 호출                                         ║
║    - Signal Broker 이벤트 브로드캐스트                                       ║
║    - SmBlock 공유 메모리 관리                                                ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Classes:                                                                    ║
║    TaskInfo      - Task 메타데이터 및 IPC 리소스                             ║
║    TaskManager   - Task 생명주기 관리자                                      ║
║    TaskRuntime   - Task 실행 래퍼 (RMI 핸들러 포함)                          ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

from __future__ import annotations
from typing import Any, Dict, Optional
from dataclasses import dataclass, field
import threading
import multiprocessing
import time
import queue
import atexit
import signal
import itertools as _itertools
import os as _os

from .task_performance import PerformanceCollector
from .task_signal import SignalBroker, SignalClient
from ._comm_utils import _sanitize_id, _check_singleton, _IS_WIN32
from ..sm_infra import SmBlockHandler, SmRingBuffer, SmValue, SmSignalRegistry
from .task_decorator import (
    rmi_task, has_run_flag, TaskValidator, _DEFAULT_RESTART_DELAY
)
from .task_error import (
    task_not_found_error, smblock_not_found_error, rmi_timeout_error,
    TaskNotFoundError, SmBlockNotFoundError, RmiTimeoutError,
)
from ._rmi_client import RmiClient, DirectClient, _DEFAULT_RMI_TIMEOUT

# ──────────────────────────────────────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────────────────────────────────────
_WORKER_JOIN_TIMEOUT = 4.0
_ext_rb_counter = _itertools.count(1)  # External SmRingBuffer 고유 ID 카운터 (thread-safe)


# ──────────────────────────────────────────────────────────────────────────────
# ShmResourceFactory — SharedMemory 리소스 표준 생성 팩토리
# ──────────────────────────────────────────────────────────────────────────────

class ShmResourceFactory:
    """SharedMemory 리소스 표준 생성 (FileExistsError 자동 복구)"""
    __slots__ = ('_prefix',)

    def __init__(self, pid: int, sanitized_id: str):
        self._prefix = f"alsk{pid}_{sanitized_id}"

    def naming(self, suffix: str) -> str:
        return f"{self._prefix}_{suffix}"

    def create_ring_buffer(self, suffix: str, size: int = 65536) -> SmRingBuffer:
        name = self.naming(suffix)
        try:
            return SmRingBuffer(name, size=size, create=True)
        except FileExistsError:
            rb = SmRingBuffer(name, size=size, create=False)
            rb._is_new = True
            rb.reset()
            return rb

    def create_value(self, suffix: str, typecode: str, initial=0) -> SmValue:
        name = self.naming(suffix)
        try:
            return SmValue(name, typecode, create=True, initial=initial)
        except FileExistsError:
            sv = SmValue(name, typecode, create=False)
            sv._is_new = True
            sv.reset(initial)
            return sv


# ──────────────────────────────────────────────────────────────────────────────
# TaskInfo
# ──────────────────────────────────────────────────────────────────────────────

@dataclass
class TaskInfo:
    task_class_name: str
    task_id: str = ""
    mode: str = "thread"
    job_class: Optional[type] = None
    injection: Dict[str, Any] = field(default_factory=dict)
    auto_restart: bool = True
    restart_delay: float = 3.0
    main_thread: bool = False  # thread 모드에서 메인 스레드 인스턴스 생성 (QObject 사용)
    job_instance: Any = None   # 메인 스레드에서 생성된 인스턴스
    # SmRingBuffer IPC (Lock-free SPSC, User-space memcpy)
    request_q: Any = None
    response_q: Any = None
    request_lock: Any = None  # multiprocessing.Lock (MPSC request writes)
    stop_flag: Any = None
    rmi_rx: Any = None
    rmi_tx: Any = None
    rmi_fail: Any = None
    rmi_time: Any = None
    rmi_time_min: Any = None
    rmi_time_max: Any = None
    restart_cnt: Any = None
    rmi_methods: Any = None
    rmi_timing: Any = None
    response_event: Any = None  # v2.6: RMI 응답 커널 이벤트 (caller 측 대기용)
    is_sleeping: Any = None    # v2.6: SmValue('B') 디스패처 대기 상태 플래그
    _ready_event: Any = None   # 동적 생성 시 초기화 완료 신호

    _SHM_FIELDS = ('request_q', 'response_q', 'response_event', 'is_sleeping',
                    'stop_flag', 'rmi_rx', 'rmi_tx', 'rmi_fail',
                    'rmi_time', 'rmi_time_min', 'rmi_time_max', 'restart_cnt')

    def __post_init__(self):
        self.task_id = self.task_id or self.task_class_name
        if not self.job_class:
            n = self.task_class_name
            self.job_class = rmi_task.get(n)
            self.mode = rmi_task.attr(n, '_tc_mode', 'thread')
            self.auto_restart = rmi_task.attr(n, '_tc_restart', True)
            self.restart_delay = rmi_task.attr(n, '_tc_delay', _DEFAULT_RESTART_DELAY)
            self.main_thread = rmi_task.attr(n, '_tc_main_thread', False)

    def close(self):
        """SharedMemory 리소스 일괄 해제 (SmRingBuffer + SmValue + SmKernelEvent)."""
        for attr in self._SHM_FIELDS:
            res = getattr(self, attr, None)
            if res and hasattr(res, 'close'):
                try: res.close()
                except Exception: pass


# ──────────────────────────────────────────────────────────────────────────────
# TaskManager
# ──────────────────────────────────────────────────────────────────────────────

class TaskManager:
    __slots__ = ('_tasks', '_workers', '_runtimes', '_mgr', '_monitor_port', '_monitor', '_gconfig',
                 '_exit_hook', '_measurement', '_performance', '_signal_broker', '_rmi_timeout',
                 '_qos_enabled', '_stopped',
                 '_task_lock', '_watchdog_running', '_watchdog_thread', '_sm_registry', '__weakref__')

    def __init__(self, gconfig):
        missing = []
        if not gconfig.data_get("app_info"): missing.append("app_info")
        if not gconfig.data_get("platform_config"): missing.append("platform_config")
        if not gconfig.data_get("task_config"): missing.append("task_config")
        if missing:
            raise ValueError(f"[TaskManager] Missing config: {', '.join(missing)}")

        self._tasks: Dict[str, TaskInfo] = {}
        self._workers: Dict[str, Any] = {}
        self._runtimes: Dict[str, 'TaskRuntime'] = {}
        self._task_lock = threading.RLock()
        self._mgr = multiprocessing.Manager()
        self._gconfig = gconfig
        self._monitor = None
        self._exit_hook = False
        self._stopped = False

        platform_config = gconfig.data_get("platform_config", {})
        config = gconfig.data_get("task_config", {})
        monitor_cfg = platform_config.get("_monitor", {}) or config.get("_monitor", {})
        self._monitor_port = monitor_cfg.get("port") if monitor_cfg else None
        self._exit_hook = monitor_cfg.get("exit_hook", False) if monitor_cfg else False
        measurement_default = monitor_cfg.get("measurement", False) if monitor_cfg else False
        _pid = _os.getpid()
        try:
            self._measurement = SmValue(f"alsk{_pid}_meas", 'b', create=True, initial=int(measurement_default))
        except FileExistsError:
            self._measurement = SmValue(f"alsk{_pid}_meas", 'b', create=False)
            self._measurement._is_new = True
            self._measurement.reset(int(measurement_default))
        rmi_cfg = platform_config.get("rmi", {})
        self._rmi_timeout = rmi_cfg.get("timeout", _DEFAULT_RMI_TIMEOUT)
        # v2.6: QoS 데드라인 체크 (기본 off — config에서 qos: true로 활성화)
        self._qos_enabled = platform_config.get("qos", False)
        mgr = self._mgr

        # LogTask 자동 등록
        log_cfg = platform_config.get("_log/log_task", {})
        if log_cfg:
            from . import task_log  # noqa: F401
            inj = {k: v for k, v in log_cfg.items() if k[0] != "@"}
            ti = TaskInfo(task_class_name="log_task", task_id="log_task", injection=inj)
            self._init_task_info(ti, mgr)
            self._tasks["log_task"] = ti

        # 사용자 Task 등록
        C, U, R = "\033[36m", "\033[4m", "\033[0m"  # Cyan, Underline, Reset
        config_path = getattr(gconfig, '_filepath', None)
        for key, cfg in config.items():
            if key.startswith("_"): continue
            # @import 처리
            if "@import" in cfg:
                try:
                    __import__(cfg["@import"])
                except ImportError as e:
                    print(f"[TaskManager] @import failed: {cfg['@import']} ({e})")
                    if config_path:
                        print(f"  {C}{U}{config_path}{R}  task: {key}")
            tid, tcn = key.split("/", 1) if "/" in key else (key, cfg.get("@task") or cfg.get("@job") or key)
            inj = {k: v for k, v in cfg.items() if k[0] != "@"}
            ti = TaskInfo(task_class_name=tcn, task_id=tid, injection=inj)
            self._init_task_info(ti, mgr)
            self._tasks[tid] = ti

        self._performance = PerformanceCollector(self._tasks, self._workers, self._measurement)
        # Signal broker: P2P Registry 전역 명부 관리자 초기화
        self._signal_broker = SignalBroker(self._mgr)
        if hasattr(self._signal_broker, 'start_relay'):
            self._signal_broker.start_relay()
        # v2.4: SmSignalRegistry (공유 메모리 비트맵 구독 관리)
        # 네임스페이스 격리: PID를 포함하여 다중 인스턴스 충돌 방지
        _reg_name = f"alaska_sig_reg_{_pid}"
        try:
            # 설계 9.3: 시작 시 기존 고아 자원 강제 정리 (Sanitization)
            self._sm_registry = SmSignalRegistry(name=_reg_name, create=True)
        except Exception:
            from multiprocessing.shared_memory import SharedMemory as _SM
            try:
                _temp_sm = _SM(name=_reg_name)
                _temp_sm.close()
                _temp_sm.unlink()
            except Exception: pass
            try:
                self._sm_registry = SmSignalRegistry(name=_reg_name, create=True)
            except Exception as e:
                print(f"[TaskManager] FATAL: SmSignalRegistry creation failed: {e}")
                self._sm_registry = None
        
        if self._sm_registry:
            self._signal_broker._sm_registry = self._sm_registry
            self._signal_broker._sm_registry_name = _reg_name
        
        # v2.3: 전역 와치독 스레드 (기본 off — config에서 watchdog: true로 활성화)
        _watchdog_enabled = platform_config.get("watchdog", False)
        if _watchdog_enabled:
            self._watchdog_running = True
            self._watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True)
            self._watchdog_thread.start()
        else:
            self._watchdog_running = False
            self._watchdog_thread = None

    def _watchdog_loop(self):
        """v2.3 전역 와치독: 쿨다운 및 무한 루프 방지 보완"""
        # 재시작 기록 관리 {tid: last_restart_ts}
        last_restarts = {}
        
        while self._watchdog_running:
            time.sleep(1.0)
            if not self._signal_broker: continue
            
            try:
                now = time.time()
                # 1. 하트비트 스냅샷 확보 (RuntimeError 방어)
                try:
                    hb_snap = dict(self._signal_broker._heartbeats)
                except (RuntimeError, Exception):
                    continue # 다음 주기에 시도
                
                for tid, last_alive in hb_snap.items():
                    # 2. 쿨다운 체크 (최근 10초 내 재시작했으면 건너뜀)
                    if now - last_restarts.get(tid, 0) < 10.0:
                        continue

                    # 3. 정지 판정 (3초 기준)
                    if (now - last_alive) > 3.0:
                        print(f"[Watchdog] Task '{tid}' frozen! Recovering...")
                        last_restarts[tid] = now
                        # 하트비트 즉시 갱신하여 중복 복구 차단
                        try: self._signal_broker._heartbeats[tid] = now
                        except Exception: pass
                        
                        self.restart_task(tid)
            except Exception as e:
                print(f"[Watchdog] Loop error: {e}")

    def _init_task_info(self, ti: TaskInfo, mgr):
        # SmRingBuffer / SmValue IPC (User-space memcpy, OS 인터럽트 없음)
        _f = ShmResourceFactory(_os.getpid(), _sanitize_id(ti.task_id))

        ti.request_q = _f.create_ring_buffer("rq")
        ti.response_q = _f.create_ring_buffer("rs")
        ti.request_lock = multiprocessing.Lock()
        ti.stop_flag = _f.create_value("sf", 'b', 0)
        ti.rmi_rx = _f.create_value("rx", 'i', 0)
        ti.rmi_tx = _f.create_value("tx", 'i', 0)
        ti.rmi_fail = _f.create_value("fl", 'i', 0)
        ti.rmi_time = _f.create_value("rt", 'd', 0.0)
        ti.rmi_time_min = _f.create_value("mn", 'd', float('inf'))
        ti.rmi_time_max = _f.create_value("mx", 'd', 0.0)
        ti.restart_cnt = _f.create_value("rc", 'i', 0)
        ti.rmi_methods = mgr.dict()
        ti.rmi_timing = mgr.dict()
        # v2.6: CPU 절감 — is_sleeping 플래그 + response_event
        ti.is_sleeping = _f.create_value("sl", 'B', 0)
        if _IS_WIN32:
            from ..sm_infra import SmKernelEvent
            _resp_evt_name = f"al_rmi_resp_{_os.getpid()}_{_sanitize_id(ti.task_id)}"
            try:
                ti.response_event = SmKernelEvent(_resp_evt_name, create=True, manual_reset=True)
            except OSError:
                ti.response_event = SmKernelEvent(_resp_evt_name, create=False, manual_reset=True)

    @property
    def performance(self) -> PerformanceCollector:
        return self._performance

    @property
    def gconfig(self):
        """Global configuration (read-only)"""
        return self._gconfig

    def generate_architecture_map(self) -> str:
        """v2.3 시스템 구성도 자동 생성 (Mermaid 포맷)"""
        lines = ["graph TD", "  subgraph ALASKA_SYSTEM"]
        
        # 1. 노드 정의 (TaskID [TaskClass])
        for tid, ti in self._tasks.items():
            mode_icon = "⚙️" if ti.mode == 'process' else "🧵"
            lines.append(f'    {tid}["{mode_icon} {tid} <br/>({ti.task_class_name})"]')

        # 2. RMI 의존성 (Injection 분석)
        for tid, ti in self._tasks.items():
            for key, val in ti.injection.items():
                if isinstance(val, str) and val.startswith("task:"):
                    target = val.split(":")[1]
                    lines.append(f'    {tid} -. RMI .-> {target}')

        # 3. 시그널 흐름 (on_ 메서드 분석)
        for tid, ti in self._tasks.items():
            cls = ti.job_class
            if not cls: continue
            try:
                for name in dir(cls):
                    if name.startswith("on_"):
                        sig_name = name[3:].replace("_", ".")
                        lines.append(f'    SIG(({sig_name})) --> {tid}')
            except Exception: pass
        
        lines.append("  end")
        return "\n".join(lines)

    def generate_sequence_map(self) -> str:
        """v2.3 정적 시퀀스 다이어그램 자동 생성 (Mermaid sequenceDiagram)

        코드 정적 분석: signal.emit() 호출(송신)과 on_ 메서드(수신)를 매칭하여
        실행 없이 예상 시그널 흐름을 시각화한다.
        """
        import re, inspect
        lines = ["sequenceDiagram"]

        # 1. Participant 정의 (TaskID)
        for tid in self._tasks:
            lines.append(f"  participant {tid}")

        # 2. emit → on_ 매칭 테이블 구축
        # 2a. on_ 수신 맵: signal_name → [task_id, ...]
        recv_map = {}  # {"raw.sensor.1": ["analytics_node"], ...}
        for tid, ti in self._tasks.items():
            cls = ti.job_class
            if not cls: continue
            for attr in dir(cls):
                if attr.startswith("on_"):
                    sig = attr[3:].replace("_", ".")
                    recv_map.setdefault(sig, []).append(tid)

        # 2b. emit 송신 분석: 소스코드에서 signal.emit("xxx", ...) 추출
        emit_pattern = re.compile(
            r'self\.signal\.emit\(\s*["\']([^"\']+)["\']'  # self.signal.emit("name"
            r'|'
            r'self\.signal\.([a-zA-Z_]\w*)\.emit\('         # self.signal.name.emit(
        )
        emit_map = {}  # {"sensor_node": ["raw.sensor.1", "sensor.urgent"], ...}
        for tid, ti in self._tasks.items():
            cls = ti.job_class
            if not cls: continue
            signals = []
            try:
                src = inspect.getsource(cls)
                for m in emit_pattern.finditer(src):
                    sig = m.group(1) or m.group(2).replace("_", ".")
                    if sig not in signals:
                        signals.append(sig)
            except Exception: pass
            if signals:
                emit_map[tid] = signals

        # 2c. RMI 의존성 (task: injection)
        rmi_map = {}  # {"controller_node": ["sensor_node"], ...}
        for tid, ti in self._tasks.items():
            for key, val in ti.injection.items():
                if isinstance(val, str) and val.startswith("task:"):
                    target = val.split(":")[1]
                    rmi_map.setdefault(tid, []).append((key, target))

        # 3. Mermaid 시퀀스 생성
        # 3a. RMI 점선 화살표
        for tid, deps in rmi_map.items():
            for proxy_name, target in deps:
                lines.append(f"  {tid} -->> {target}: RMI ({proxy_name})")

        # 3b. Signal 실선 화살표 (emit → on_ 매칭)
        for tid, sigs in emit_map.items():
            for sig in sigs:
                receivers = recv_map.get(sig, [])
                if receivers:
                    for rtid in receivers:
                        lines.append(f"  {tid} ->> {rtid}: {sig}")
                else:
                    lines.append(f"  Note over {tid}: {sig} (no subscriber)")

        return "\n".join(lines)

    def _create_worker(self, task_id, ti):
        """★ Phase 3: TaskRuntime + Worker 생성 (process/thread 공통 팩토리)"""
        if ti.mode == 'process':
            import copy
            clean_tasks = {}
            for t_id, t_info in self._tasks.items():
                clean_ti = copy.copy(t_info)
                clean_ti.job_instance = None
                clean_ti.response_event = None
                clean_tasks[t_id] = clean_ti
            w = TaskRuntime(clean_tasks[task_id], clean_tasks,
                            measurement=self._measurement,
                            signal_broker=self._signal_broker,
                            rmi_timeout=self._rmi_timeout,
                            gconfig=self._gconfig,
                            manager=None)
            w._qos_enabled = self._qos_enabled
            h = multiprocessing.Process(target=w.run, daemon=True)
        else:
            w = TaskRuntime(ti, self._tasks,
                            measurement=self._measurement,
                            signal_broker=self._signal_broker,
                            rmi_timeout=self._rmi_timeout,
                            gconfig=self._gconfig,
                            manager=self)
            w._qos_enabled = self._qos_enabled
            h = threading.Thread(target=w.run, daemon=True)
        return w, h

    def start_all(self, exit_on_duplicate: bool = True):
        if self._monitor_port and not _check_singleton(self._monitor_port):
            raise SystemExit(1) if exit_on_duplicate else RuntimeError(f"Port {self._monitor_port} in use")

        validator = TaskValidator()
        for tid, ti in self._tasks.items():
            validator.add(tid, ti.task_class_name)
        if not validator.validate():
            validator.print_errors()
            raise SystemExit(1)

        self._print_task_table()
        SmBlockHandler.creation(self._gconfig, self._mgr, self._tasks)

        if self._signal_broker:
            _pid = _os.getpid()
            for tid, ti in self._tasks.items():
                self._signal_broker.register_inbox(tid, mode=ti.mode)
                # v2.4: SmSignalRegistry에 태스크 등록
                if self._sm_registry:
                    self._sm_registry.register_task(tid)
                # v2.4: notify_event 생성 (SmKernelEvent/mp.Event)
                _sid = _sanitize_id(tid)
                if _IS_WIN32:
                    from ..sm_infra import SmKernelEvent
                    _evt_name = f"al_notify_{_pid}_{_sid}"
                    try:
                        ev = SmKernelEvent(_evt_name, create=True, manual_reset=True)
                    except OSError:
                        ev = SmKernelEvent(_evt_name, create=False, manual_reset=True)
                else:
                    ev = multiprocessing.Event()
                self._signal_broker._notify_events[tid] = ev
                # v2.5: 이벤트 이름 등록 (크로스프로세스 lazy re-open용)
                if _IS_WIN32:
                    self._signal_broker._notify_event_names[tid] = _evt_name
                # v2.6: is_sleeping 플래그 등록
                if ti.is_sleeping is not None:
                    self._signal_broker._sleeping_flags[tid] = ti.is_sleeping
                # 사전 구독: 모든 Task의 on_* 핸들러를 Registry에 즉시 반영
                if ti.job_class:
                    self._pre_subscribe_signals(tid, ti.job_class)

        for tid, ti in self._tasks.items():
            if tid not in self._workers:
                # main_thread=True인 경우, 메인 스레드에서 인스턴스 생성 (QObject 지원)
                if ti.mode == 'thread' and ti.main_thread and ti.job_class:
                    ti.job_instance = ti.job_class()  # 메인 스레드에서 생성
                    ti.job_instance._input_queue = queue.Queue()  # 기본 입력 큐 주입
                    if not hasattr(ti.job_instance, 'on_input'):
                        ti.job_instance.on_input = lambda data, j=ti.job_instance: j._input_queue.put_nowait(data)
                w, h = self._create_worker(tid, ti)
                if ti.mode == 'thread':
                    self._runtimes[tid] = w  # thread 모드는 런타임 저장 (get_task용)
                h.start()
                self._workers[tid] = h

        if self._monitor_port and not self._monitor:
            from ..monitor.task_monitor import TaskMonitor
            self._monitor = TaskMonitor(self, self._monitor_port, gconfig=self._gconfig)
            self._monitor.start()

        if self._exit_hook:
            atexit.register(self.stop_all)
            if threading.current_thread() is threading.main_thread():
                signal.signal(signal.SIGINT, lambda s, f: self.stop_all())

    def stop_all(self, skip_monitor: bool = False):
        if self._stopped:
            return
        self._stopped = True
        from ._banner import banner
        banner("ALASKA v2.0 Kernel Platform - Shutdown", [
            "All services are being terminated.",
            "The system will be fully down within 5 seconds.",
            "",
            "Copyright (c) 2014-2026 Dongil Vision Korea. All Rights Reserved.",
        ])
        # Watchdog 종료
        self._watchdog_running = False
        if self._watchdog_thread:
            self._watchdog_thread.join(timeout=2.0)
            self._watchdog_thread = None
        if self._monitor and not skip_monitor:
            self._monitor.stop()
            self._monitor = None
        with self._task_lock:
            tasks_snapshot = dict(self._tasks)
            workers_snapshot = dict(self._workers)
        for ti in tasks_snapshot.values():
            ti.stop_flag.value = True
        for tid, w in workers_snapshot.items():
            w.join(timeout=_WORKER_JOIN_TIMEOUT)
            if w.is_alive() and hasattr(w, 'terminate'):
                w.terminate()
        # SharedMemory cleanup (SmRingBuffer + SmValue)
        for ti in tasks_snapshot.values():
            ti.close()
        # _measurement cleanup
        if hasattr(self._measurement, 'close'):
            try: self._measurement.close()
            except Exception: pass
        # v2.4: SmSignalRegistry cleanup
        if hasattr(self, '_sm_registry') and self._sm_registry:
            try: self._sm_registry.close()
            except Exception: pass
            self._sm_registry = None
        # v2.4: notify_events cleanup
        if self._signal_broker:
            for ev in self._signal_broker._notify_events.values():
                if hasattr(ev, 'close'):
                    try: ev.close()
                    except Exception: pass
            self._signal_broker._notify_events.clear()
        # v2.5: Signal SmRingBuffer cleanup
        if self._signal_broker:
            for tid, qs in list(self._signal_broker._cache.q_cache.items()):
                if qs:
                    for sq in qs:
                        if hasattr(sq, 'close'):
                            try: sq.close()
                            except Exception: pass
            self._signal_broker._cache.q_cache.clear()
        with self._task_lock:
            self._workers.clear()
            self._runtimes.clear()
        SmBlockHandler.close_all()

    def restart_task(self, task_id: str):
        """Watchdog에서 호출: frozen task 재시작"""
        with self._task_lock:
            ti = self._tasks.get(task_id)
            worker = self._workers.get(task_id)
            if not ti or not worker:
                return

            # 1. 기존 프로세스/스레드 정지
            if ti.mode == 'process':
                if hasattr(worker, 'terminate'):
                    worker.terminate()
                worker.join(timeout=1.0)
                if worker.is_alive() and hasattr(worker, 'kill'):
                    worker.kill()
            else:
                ti.stop_flag.value = True
                if isinstance(worker, threading.Thread):
                    worker.join(timeout=1.0)

            # 2. 큐 데이터 청소 (v2.5: SmRingBuffer reset 우선)
            if self._signal_broker:
                try:
                    qs = self._signal_broker._cache.q_cache.get(task_id)
                    if qs is None:
                        qs = self._signal_broker._queues.get(task_id)
                    if qs:
                        for q in qs:
                            if hasattr(q, 'reset'):
                                q.reset()
                            else:
                                while not q.empty():
                                    try: q.get_nowait()
                                    except Exception: break
                except Exception: pass

            # 3. SmRingBuffer 초기화
            for q in (ti.request_q, ti.response_q):
                if q and hasattr(q, 'reset'):
                    try: q.reset()
                    except Exception: pass

            # 4. 상태 초기화 및 재시작
            ti.stop_flag.value = False
            ti.restart_cnt.value += 1

            w, h = self._create_worker(task_id, ti)
            h.start()
            self._workers[task_id] = h
            if ti.mode == 'thread':
                self._runtimes[task_id] = w

    # ── 동적 Task 생성/삭제 (Phase 1: thread 모드 전용) ──────────────────

    def add_task(self, task_id: str, task_class_name: str,
                 injection: Dict[str, Any] = None) -> bool:
        """실행 중 Task 동적 추가 (thread 모드 전용)

        Args:
            task_id: 고유 태스크 ID
            task_class_name: @rmi_task 등록명
            injection: 의존성 주입 딕셔너리

        Returns:
            True: 성공, False: 이미 존재
        """
        with self._task_lock:
            if task_id in self._tasks:
                print(f"[TaskManager] add_task: '{task_id}' already exists")
                return False

        # TaskInfo 생성
        ti = TaskInfo(task_class_name=task_class_name, task_id=task_id,
                      injection=injection or {})
        if not ti.job_class:
            raise ValueError(f"[TaskManager] add_task: class '{task_class_name}' not registered. Use @rmi_task decorator.")
        if ti.mode == 'process':
            raise ValueError(f"[TaskManager] add_task: process mode not supported (Phase 1). Use thread mode.")

        # IPC 리소스 할당
        self._init_task_info(ti, self._mgr)

        # ready_event: 초기화 완료 대기용
        ti._ready_event = threading.Event()

        # 등록
        with self._task_lock:
            if task_id in self._tasks:  # double-check
                return False
            self._tasks[task_id] = ti

        # Signal 등록 (v2.1: Registry에 Inbox 등록 및 동기화 트리거)
        if self._signal_broker:
            self._signal_broker.register_inbox(task_id, mode=ti.mode)

        # Worker 생성 & 시작
        w = TaskRuntime(ti, self._tasks,
                        measurement=self._measurement,
                        signal_broker=self._signal_broker,
                        rmi_timeout=self._rmi_timeout,
                        gconfig=self._gconfig,
                        manager=self)
        w._qos_enabled = self._qos_enabled
        h = threading.Thread(target=w.run, daemon=True)
        with self._task_lock:
            self._runtimes[task_id] = w
            self._workers[task_id] = h
        h.start()

        # 초기화 완료 대기
        if not ti._ready_event.wait(timeout=5.0):
            print(f"[W:TaskManager] add_task timeout: {task_id} (continuing async)")

        print(f"[TaskManager] Task added: {task_id} ({task_class_name})")
        return True

    def remove_task(self, task_id: str, timeout: float = 5.0) -> bool:
        """실행 중 Task 동적 삭제

        Args:
            task_id: 삭제할 태스크 ID
            timeout: 종료 대기 시간 (초)

        Returns:
            True: 성공, False: 존재하지 않음
        """
        with self._task_lock:
            ti = self._tasks.get(task_id)
            worker = self._workers.get(task_id)
        if not ti:
            print(f"[TaskManager] remove_task: '{task_id}' not found")
            return False

        # 의존성 경고
        with self._task_lock:
            for tid, t in self._tasks.items():
                if tid == task_id:
                    continue
                for k, v in t.injection.items():
                    if isinstance(v, str) and v == f"client:{task_id}":
                        print(f"[W:TaskManager] Task '{tid}' depends on '{task_id}' (injection: {k})")

        # 종료 요청
        ti.stop_flag.value = True

        # Signal 해제
        if self._signal_broker:
            self._signal_broker.unregister(task_id)

        # Worker 종료 대기
        if worker:
            worker.join(timeout=timeout)
            if worker.is_alive():
                print(f"[W:TaskManager] remove_task: '{task_id}' join timeout")

        # SharedMemory close (SmRingBuffer + SmValue + SmKernelEvent)
        ti.close()

        # 등록 해제
        with self._task_lock:
            self._tasks.pop(task_id, None)
            self._workers.pop(task_id, None)
            self._runtimes.pop(task_id, None)

        print(f"[TaskManager] Task removed: {task_id}")
        return True

    def clear_stats(self):
        inf = float('inf')
        with self._task_lock:
            snapshot = list(self._tasks.values())
        for ti in snapshot:
            ti.rmi_rx.value = ti.rmi_tx.value = ti.rmi_fail.value = 0
            ti.rmi_time.value = ti.rmi_time_max.value = 0.0
            ti.rmi_time_min.value = inf
            ti.restart_cnt.value = 0
            ti.rmi_methods.clear()
            ti.rmi_timing.clear()

    def get_status(self) -> Dict[str, Dict[str, Any]]:
        result = {}
        inf = float('inf')
        with self._task_lock:
            tasks_snapshot = dict(self._tasks)
            workers_snapshot = dict(self._workers)
        for tid, ti in tasks_snapshot.items():
            w = workers_snapshot.get(tid)
            try:
                tx = int(ti.rmi_tx.value)
                tt = float(ti.rmi_time.value)
                tmin = float(ti.rmi_time_min.value)
                result[tid] = {
                    'class': ti.task_class_name, 'mode': ti.mode,
                    'alive': w.is_alive() if w else False,
                    'rmi_rx': int(ti.rmi_rx.value), 'rmi_tx': tx, 'rmi_fail': int(ti.rmi_fail.value),
                    'rmi_avg': tt / tx if tx else 0.0,
                    'rmi_min': 0.0 if tmin == inf else tmin,
                    'rmi_max': float(ti.rmi_time_max.value),
                    'rxq_size': ti.request_q._used() if not getattr(ti.request_q, '_closed', True) else 0,
                    'txq_size': ti.response_q._used() if not getattr(ti.response_q, '_closed', True) else 0,
                    'restart': int(ti.restart_cnt.value),
                    'rmi_methods': dict(ti.rmi_methods),
                }
            except Exception:
                result[tid] = {
                    'class': ti.task_class_name, 'mode': ti.mode,
                    'alive': w.is_alive() if w else False,
                }
        return result

    def get_client(self, task_id: str):
        """Task RMI 클라이언트 생성 (External용)

        Thread 모드 → DirectClient (M-01: 직접 호출, IPC 없음)
        Process 모드 → RmiClient (SmRingBuffer IPC)
        """
        ti = self._tasks.get(task_id)
        if not ti:
            task_not_found_error(task_id, self._tasks, "get_client")
            raise TaskNotFoundError(task_id, self._tasks)

        # M-01: Thread 모드 → DirectClient (IPC 없이 직접 호출)
        if ti.mode == 'thread':
            _tasks = self._tasks
            _runtimes = self._runtimes
            def _job_getter():
                _ti = _tasks.get(task_id)
                if _ti and _ti.job_instance:
                    return _ti.job_instance
                _rt = _runtimes.get(task_id)
                return getattr(_rt, 'job', None) if _rt else None
            return DirectClient(task_id, _job_getter,
                                broker=self._signal_broker)

        # Process 모드 → RmiClient (SmRingBuffer IPC)
        _cnt = next(_ext_rb_counter)
        _pid = _os.getpid()
        _sid = _sanitize_id(task_id)
        _name = f"alsk{_pid}_ext{_cnt}_{_sid}"
        try:
            response_rb = SmRingBuffer(_name, size=65536, create=True)
        except FileExistsError:
            response_rb = SmRingBuffer(_name, size=65536, create=False)
            response_rb._is_new = True
            response_rb.reset()
        # ★ Target의 notify_event 조회 (InvokeDispenser 깨움용)
        _ne = None
        if self._signal_broker:
            _ne = self._signal_broker._notify_events.get(task_id)
            if _ne is None:
                _ne = self._signal_broker._notify_cache.get(task_id)
        return RmiClient(
            target_id=task_id,
            request_q=ti.request_q,
            request_lock=ti.request_lock,
            response_q=response_rb,
            timeout=self._rmi_timeout,
            broker=self._signal_broker,
            notify_event=_ne,
            target_sleeping=ti.is_sleeping
        )

    def get_task(self, task_id: str):
        """thread 모드 Task 인스턴스 반환 (Qt Signal 직접 접근용)"""
        runtime = self._runtimes.get(task_id)
        if not runtime:
            ti = self._tasks.get(task_id)
            if not ti:
                self._print_get_task_error(task_id, "Task not found")
                return None
            if ti.mode == 'process':
                self._print_get_task_error(task_id, "process mode Task. Use get_client() instead.")
                return None
            # main_thread=True인 경우 start_all()에서 이미 인스턴스 생성됨
            if ti.job_instance is not None:
                return ti.job_instance
            return None  # 아직 시작되지 않음
        return getattr(runtime, 'job', None) or getattr(self._tasks.get(task_id), 'job_instance', None)

    def _print_get_task_error(self, task_id: str, message: str):
        """get_task() 오류 메시지 출력 (배너 + 호출 위치 링크)"""
        import inspect
        # ANSI 색상 코드
        B = "\033[92m"  # 밝은 녹색 (테두리)
        YELLOW = "\033[33m"
        CYAN = "\033[36m"
        UNDERLINE = "\033[4m"
        RESET = "\033[0m"
        W = 72  # 배너 너비
        # 호출 스택에서 get_task() 호출 위치 찾기
        # _print_get_task_error → get_task → caller
        frame = inspect.currentframe()
        try:
            caller_frame = frame.f_back.f_back  # caller of get_task()
            filename = caller_frame.f_code.co_filename
            lineno = caller_frame.f_lineno
        except (AttributeError, ValueError):
            filename, lineno = "unknown", 0
        finally:
            del frame
        # 배너 출력 (가는 실선)
        print()
        print(f"{B}┌{'─' * W}┐{RESET}")
        # 오류 메시지
        err_text = f"get_task('{task_id}'): {message}"
        err_text = err_text if len(err_text) <= W - 4 else err_text[:W - 7] + "..."
        print(f"{B}│{RESET}  {YELLOW}{err_text}{RESET}{' ' * (W - len(err_text) - 2)}{B}│{RESET}")
        # 호출 위치 링크
        link = f"{filename}:{lineno}"
        link_display = f"  → {CYAN}{UNDERLINE}{link}{RESET}"
        visible_len = 4 + len(link)
        print(f"{B}│{RESET}{link_display}{' ' * (W - visible_len)}{B}│{RESET}")
        print(f"{B}└{'─' * W}┘{RESET}")
        self._print_task_table()

    def get_signal_client(self, client_id: str = "_external") -> 'SignalClient':
        """외부 Signal 수신용 SignalClient 생성"""
        if not self._signal_broker:
            raise RuntimeError("SignalBroker not initialized")
        return SignalClient(self._signal_broker, client_id)

    def get_smblock(self, name: str) -> 'SmBlock':
        """SmBlock 인스턴스 반환"""
        pool = SmBlockHandler._pools.get(name)
        if not pool:
            available = list(SmBlockHandler._pools.keys())
            smblock_not_found_error(name, available)
            raise SmBlockNotFoundError(name, available)
        return pool

    def get_smblock_names(self) -> list:
        """등록된 모든 SmBlock 이름 반환"""
        return list(SmBlockHandler._pools.keys())

    def get_buffer(self, smblock_name: str, copy: bool = False):
        """SmBlock 버퍼 컨텍스트 매니저 (with문으로 자동 할당/해제)"""
        return self.get_smblock(smblock_name).buffer(copy=copy)

    def _pre_subscribe_signals(self, task_id: str, job_class: type):
        """Process 모드 Task의 시그널 구독을 메인 브로커에 사전 등록

        Process 모드 Task는 subprocess에서 실행되며, pickle된 broker 사본을 사용.
        subprocess에서 broker.subscribe()해도 main broker의 _sub_snapshot에 반영되지 않아
        relay를 통한 시그널 분배가 실패함. 이를 방지하기 위해 main broker에 미리 등록.
        """
        broker = self._signal_broker
        # 1. @rmi_signal 데코레이터
        for name in dir(job_class):
            if name.startswith('__'):
                continue
            method = getattr(job_class, name, None)
            if callable(method) and hasattr(method, '_signal_handler'):
                broker.subscribe(method._signal_handler, task_id)
        # 2. signal_forward (RMI Signal → Qt Signal)
        forward = getattr(job_class, '_rmi_signal_forward', {}) or {}
        for signal_name in forward:
            broker.subscribe(signal_name, task_id)
        # 3. _tc_signal_subscribe (on_* 자동 감지 + 명시적 목록)
        subscribe_list = getattr(job_class, '_tc_signal_subscribe', []) or []
        for signal_name in subscribe_list:
            broker.subscribe(signal_name, task_id)

    def _print_task_table(self):
        headers = ['ID', 'Mode', 'Class', 'rmi_name', 'rmi_run']
        rows = []
        for tid, ti in self._tasks.items():
            cls = ti.job_class
            rmi_name = getattr(cls, '_tc_name', '-') if cls else '-'
            rmi_run = self._find_rmi_run_method(cls) if cls else '-'
            mode_str = ti.mode + ('(M)' if ti.main_thread else '')  # M = main_thread
            rows.append([tid, mode_str, ti.task_class_name, rmi_name, rmi_run])
        widths = [max(len(h), max((len(str(r[i])) for r in rows), default=0)) for i, h in enumerate(headers)]
        top    = '┌' + '┬'.join('─' * (w + 2) for w in widths) + '┐'
        mid    = '├' + '┼'.join('─' * (w + 2) for w in widths) + '┤'
        bottom = '└' + '┴'.join('─' * (w + 2) for w in widths) + '┘'
        print('\n' + top)
        print('│' + '│'.join(f' {h:<{w}} ' for h, w in zip(headers, widths)) + '│')
        print(mid)
        for row in rows:
            print('│' + '│'.join(f' {str(c):<{w}} ' for c, w in zip(row, widths)) + '│')
        print(bottom + '\n')

    def _find_rmi_run_method(self, cls: type) -> str:
        for name in dir(cls):
            if name.startswith('__'): continue
            method = getattr(cls, name, None)
            if callable(method) and has_run_flag(method):
                return name
        if hasattr(cls, 'run') and callable(getattr(cls, 'run', None)):
            return 'run'
        if hasattr(cls, 'rmi_loop') and callable(getattr(cls, 'rmi_loop', None)):
            return 'rmi_loop'
        return '-'

    def __enter__(self): self.start_all(); return self
    def __exit__(self, *_): self.stop_all(); return False



# ──────────────────────────────────────────────────────────────────────────────
# TaskRuntime (→ _task_runtime.py)
# ──────────────────────────────────────────────────────────────────────────────
from ._task_runtime import TaskRuntime  # noqa: E402
