# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""TaskRuntime — Task 실행 래퍼 (RMI 핸들러 포함)

task_manager.py에서 분리: TaskRuntime, _invoke_dispenser (Signal+RMI 통합 디스패처).
"""

from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict
import threading
import time
import traceback
import pickle
import queue
import os as _os
import sys as _sys
import warnings as _warnings

from .task_performance import TimingRecorder
from .task_signal import SignalClient, _SignalEmitter
from ._comm_utils import _sanitize_id, _IS_WIN32
from ..sm_infra import SmBlockHandler, SmSignalRegistry
from .task_decorator import has_run_flag, get_signal_handlers, get_rmi_signal_forward
from ._rmi_client import (
    RmiProtocol, RmiClient, DirectClient,
    _rmi_chain_local, _MISSING,
    _DEFAULT_RMI_TIMEOUT, _MAX_WRITE_RETRY,
)

if TYPE_CHECKING:
    from .task_manager import TaskInfo


class TaskRuntime:
    __slots__ = ('ti', 'tasks', 'job', '_run', '_th', '_measurement', '_timing_recorder',
                 '_signal_broker', '_signal_client', '_rmi_timeout', 'log',
                 '_debug_msg', '_debug_signal', '_debug_method', '_debug_variable', '_debug_task',
                 '_gconfig', '_qos_enabled', '_manager')

    def __init__(self, ti: TaskInfo, tasks: Dict[str, TaskInfo], measurement=None, signal_broker=None, rmi_timeout: float = _DEFAULT_RMI_TIMEOUT, gconfig=None, manager=None):
        self.ti, self.tasks, self.job, self._run, self._th = ti, tasks, None, True, None
        self._measurement = measurement
        self._rmi_timeout = rmi_timeout
        self._timing_recorder = None  # run()에서 초기화 (pickle 호환)
        self._signal_broker = signal_broker
        self._signal_client = None
        self.log = None  # run()에서 초기화 (pickle 호환)
        self._debug_msg = False  # run()에서 설정
        self._debug_signal = False  # run()에서 설정
        self._debug_method = False  # run()에서 설정
        self._debug_variable = False  # run()에서 설정
        self._debug_task = False  # run()에서 설정
        self._gconfig = gconfig
        self._qos_enabled = False
        self._manager = manager

    @property
    def manager(self):
        """TaskManager 인스턴스 반환 (Thread 모드 전용)"""
        if self.ti.mode != 'thread':
            return None
        return self._manager

    @property
    def gconfig(self):
        """Global configuration 반환"""
        return self._gconfig

    def __getstate__(self):
        """Pickle 직렬화: unpicklable 객체 제외"""
        return {
            'ti': self.ti,
            'tasks': self.tasks,
            'job': None,
            '_run': self._run,
            '_th': None,
            '_measurement': self._measurement,
            '_timing_recorder': None,
            '_signal_broker': self._signal_broker,
            '_signal_client': None,
            '_rmi_timeout': self._rmi_timeout,
            'log': None,
            '_debug_msg': False,
            '_debug_signal': False,
            '_debug_method': False,
            '_debug_variable': False,
            '_debug_task': False,
            '_gconfig': self._gconfig,
            '_qos_enabled': self._qos_enabled,
        }

    def __setstate__(self, state):
        """Pickle 역직렬화"""
        self.ti = state['ti']
        self.tasks = state['tasks']
        self.job = state['job']
        self._run = state['_run']
        self._th = state['_th']
        self._measurement = state['_measurement']
        self._timing_recorder = state['_timing_recorder']
        self._signal_broker = state['_signal_broker']
        self._signal_client = state['_signal_client']
        self._rmi_timeout = state['_rmi_timeout']
        self.log = state['log']
        self._debug_msg = state.get('_debug_msg', False)
        self._debug_signal = state.get('_debug_signal', False)
        self._debug_method = state.get('_debug_method', False)
        self._debug_variable = state.get('_debug_variable', False)
        self._debug_task = state.get('_debug_task', False)
        self._gconfig = state.get('_gconfig')
        self._qos_enabled = state.get('_qos_enabled', False)
        self._manager = None

    def run(self):
        ti = self.ti
        # pickle 후 재초기화 (process 모드)
        if self._timing_recorder is None:
            self._timing_recorder = TimingRecorder(ti.rmi_methods, ti.rmi_timing)
        if self.log is None:
            from .task_log import RmiLogger
            self.log = RmiLogger(self)
        if not ti.job_class:
            print(f"[W:{ti.task_id}] no job_class"); return
        # main_thread=True인 경우 메인 스레드에서 생성된 인스턴스 사용 (QObject 지원)
        if ti.job_instance is not None:
            self.job = ti.job_instance
        else:
            self.job = ti.job_class()  # __init__(self) 호출
            self.job._input_queue = queue.Queue()  # 기본 입력 큐 주입
            # on_input 메서드 자동 생성 (미정의 시)
            if not hasattr(self.job, 'on_input'):
                self.job.on_input = lambda data: self.job._input_queue.put_nowait(data)
            # Thread 모드: DirectClient가 참조할 수 있도록 저장
            if ti.mode == 'thread':
                ti.job_instance = self.job
        self.job.runtime = self   # runtime 속성 주입
        # self.print() 디버그 출력 메서드 바인딩
        _tid_prefix = f"[{ti.task_id:>12s}]"
        _rt = self
        def _debug_print(*args, sep=' ', end='\n'):
            if not _rt._debug_msg:
                return
            msg = sep.join(str(a) for a in args)
            _sys.stdout.write(f"{_tid_prefix} MSG  | {msg}{end}")
            _sys.stdout.flush()
        self.job.print = _debug_print
        def _debug_exception(e: Exception):
            tb = e.__traceback__
            frames = traceback.extract_tb(tb)
            if frames:
                last = frames[-1]
                loc = f"{last.filename}:{last.lineno}"
                _sys.stdout.write(f"{_tid_prefix} ERR  | {type(e).__name__}: {e}\n")
                for f in frames:
                    _sys.stdout.write(f"{_tid_prefix}      |   {f.filename}:{f.lineno} in {f.name}\n")
            else:
                _sys.stdout.write(f"{_tid_prefix} ERR  | {type(e).__name__}: {e}\n")
            _sys.stdout.flush()
        self.job.exception = _debug_exception

        # Device Property 초기화
        if hasattr(self.job, "_device_init"):
            self.job._device_init()

        # debug 파싱: config.json 우선, 없으면 @task 데코레이터
        _cfg_debug = ti.injection.get('debug', None)
        debug_cfg = _cfg_debug if _cfg_debug is not None else getattr(ti.job_class, '_tc_debug', False)
        if debug_cfg is True:
            self._debug_msg = self._debug_signal = self._debug_method = self._debug_variable = self._debug_task = True
        elif isinstance(debug_cfg, str):
            opts = {o.strip().lower() for o in debug_cfg.split(',')}
            _all = 'all' in opts
            self._debug_msg = _all or 'msg' in opts
            self._debug_signal = _all or 'signal' in opts or 'sig' in opts
            self._debug_method = _all or 'method' in opts or 'ipc' in opts or 'rmi' in opts
            self._debug_variable = _all or 'variable' in opts or 'var' in opts
            self._debug_task = _all or 'task' in opts
        if self._signal_broker:
            # v2.4: 서브프로세스에서 SmSignalRegistry 재연결
            if ti.mode == 'process' and self._signal_broker._sm_registry is None:
                reg_name = getattr(self._signal_broker, '_sm_registry_name', None)
                if reg_name:
                    try:
                        self._signal_broker._sm_registry = SmSignalRegistry(name=reg_name, create=False)
                    except Exception:
                        pass
            # v2.4: notify_event 조회/재연결
            _notify_ev = self._signal_broker._notify_events.get(ti.task_id)
            if _notify_ev is None and ti.mode == 'process':
                # 서브프로세스: SmKernelEvent re-open (Win32) 또는 pickle된 mp.Event
                if _IS_WIN32:
                    _sid = _sanitize_id(ti.task_id)
                    _evt_name = (getattr(self._signal_broker, '_sm_registry_name', '') or '').replace('alaska_sig_reg_', '')
                    if _evt_name:
                        try:
                            from ..sm_infra import SmKernelEvent
                            _notify_ev = SmKernelEvent(f"al_notify_{_evt_name}_{_sid}", create=False, manual_reset=True)
                        except Exception:
                            _notify_ev = None
            # v2.6: response_event 재연결 (process 모드 — ctypes SmKernelEvent pickle 불가)
            #        자신 + 다른 모든 태스크의 response_event를 재연결 (중첩호출 시 caller 깨움 필요)
            if ti.mode == 'process' and _IS_WIN32:
                _pid_str = (getattr(self._signal_broker, '_sm_registry_name', '') or '').replace('alaska_sig_reg_', '')
                if _pid_str:
                    from ..sm_infra import SmKernelEvent
                    for _tid, _tinfo in self.tasks.items():
                        if _tinfo.response_event is not None:
                            continue
                        _s = _sanitize_id(_tid)
                        try:
                            _tinfo.response_event = SmKernelEvent(f"al_rmi_resp_{_pid_str}_{_s}", create=False, manual_reset=True)
                        except Exception:
                            _tinfo.response_event = None
            self._signal_client = SignalClient(self._signal_broker, ti.task_id, mode=ti.mode,
                                               debug=self._debug_signal, notify_event=_notify_ev,
                                               qos_enabled=self._qos_enabled)
            self._signal_client._job = self.job
            # ★ start() 호출 안 함 — InvokeDispenser가 Signal IO 스레드 대체
            self._auto_subscribe_signals()
            self.job.signal = _SignalEmitter(self._signal_client)  # signal 체인 접근자 주입
        # ★ InvokeDispenser 시작 (RMI + Signal 통합, 태스크당 2스레드)
        self._th = threading.Thread(target=self._invoke_dispenser, daemon=True)
        self._th.start()
        self._inject_dependencies()
        if not self._validate_injection():
            if self._signal_client: self._signal_client.stop()
            if ti._ready_event: ti._ready_event.set()
            return
        # 초기화 완료 신호 (동적 생성 시 add_task()가 대기 중)
        if ti._ready_event:
            ti._ready_event.set()
        self._run_main_loop()
        # ★ InvokeDispenser 정지
        self._run = False
        if self._signal_client and self._signal_client._notify_event:
            try: self._signal_client._notify_event.set()  # 커널 대기 깨움
            except Exception: pass
        if self._th:
            self._th.join(timeout=1.0)
        if self._signal_client:
            self._signal_client.off_all()
            self._signal_client.stop()

    def _validate_injection(self) -> bool:
        ti, job = self.ti, self.job
        errors = []
        if 'smblock' in ti.injection and (not hasattr(job, 'smblock') or job.smblock is None):
            errors.append("smblock not injected")
        if 'next' in ti.injection and (not hasattr(job, 'next') or job.next is None):
            errors.append("next not injected")
        if errors:
            print(f"[W:{ti.task_id}] ERROR: {', '.join(errors)}")
            time.sleep(3)
            return False
        return True

    def _auto_subscribe_signals(self):
        if not self._signal_client or not self.job: return
        registered = {}  # {signal_name: [handler_info, ...]}

        # @rmi_signal 데코레이터 처리
        for signal_name, method in get_signal_handlers(self.job):
            self._signal_client.on(signal_name, method)
            registered.setdefault(signal_name, []).append(f"@rmi_signal:{method.__name__}")

        # _rmi_signal_forward 선언적 매핑 처리 (RMI Signal → Qt Signal)
        # @rmi_signal과 동일 시그널이어도 BOTH 핸들러 등록 (둘 다 실행)
        bridge = get_rmi_signal_forward(self.job)
        for rmi_signal_name, qt_signal_attr in bridge.items():
            qt_signal = getattr(self.job, qt_signal_attr, None)
            if qt_signal is not None and hasattr(qt_signal, 'emit'):
                def make_handler(qs, attr_name):
                    def handler(sig):
                        qs.emit(sig.data)
                    handler.__name__ = f"bridge:{attr_name}"
                    return handler
                self._signal_client.on(rmi_signal_name, make_handler(qt_signal, qt_signal_attr))
                registered.setdefault(rmi_signal_name, []).append(f"signal_forward:{qt_signal_attr}")

        # signal_subscribe: 시그널 이름 규칙 기반 자동 핸들러 매핑
        # signal.{path}.emit(data) → on_{path_with_underscore}(data)
        # 예: "measurement.start" → on_measurement_start(data)
        subscribe_list = getattr(self.job.__class__, '_tc_signal_subscribe', []) or []
        for signal_name in subscribe_list:
            if signal_name in registered:
                continue  # 이미 @rmi_signal 또는 signal_forward로 등록됨
            # 메서드 이름 변환: measurement.start → on_measurement_start
            method_name = "on_" + signal_name.replace(".", "_")
            handler = getattr(self.job, method_name, None)
            if handler and callable(handler):
                self._signal_client.on(signal_name, handler)
                registered.setdefault(signal_name, []).append(f"signal_subscribe:{method_name}")
            else:
                # 핸들러 없으면 구독만 (브로커에 등록)
                self._signal_client._broker.subscribe(signal_name, self.ti.task_id)
                registered.setdefault(signal_name, []).append("signal_subscribe(no handler)")

        # 등록된 핸들러 로그 출력
        if registered:
            print(f"[{self.ti.task_id}] Signal subscriptions: {list(registered.keys())}")
        for sig_name, handlers in registered.items():
            if len(handlers) > 1:
                print(f"[{self.ti.task_id}] Signal '{sig_name}' handlers: {handlers} (BOTH will execute)")

    def _invoke_dispenser(self):
        """InvokeDispenser v2.6: 단일 스레드 통합 디스패처 (IPC/RMI + Signal)

        설계서: doc/2016.InvokeDispenser설계.txt

        루프 순서:
          ① Signal RECV (QoS P1→P5) — ★ 우선
          ② IPC/RMI (request_q, 5건/cycle) — Signal 후순
          ③ Kernel Wait (idle 시 CPU 0%)
        SEND는 v2.6에서 제거 — emit()이 caller 스레드에서 직접 처리
        """
        ti, job, tasks = self.ti, self.job, self.tasks
        sc = self._signal_client
        task_id = ti.task_id

        # ── 입력 소스 ──
        rb = ti.request_q                     # SmRingBuffer: RMI 요청
        stop = ti.stop_flag
        qs = sc._queues                       # QoS P1~P5 (5-tuple)
        scheduler = sc._scheduler
        notify_event = sc._notify_event
        broker = sc._broker

        # v2.6: is_sleeping 플래그 (CPU 절감)
        _is_sleeping = ti.is_sleeping

        # ── 로컬 바인딩 (LOP-1) ──
        _rb_read = rb.read
        _rb_used = rb._used
        _pickle_loads = pickle.loads
        _pickle_dumps = pickle.dumps
        _tasks_get = tasks.get
        # Phase 1: Binary protocol 로컬 바인딩 (→ RmiProtocol)
        _BIN_REQ_L = RmiProtocol.BIN_REQ
        _BIN_RESP_L = RmiProtocol.BIN_RESP
        _st_bin_req_unpack = RmiProtocol.ST_REQ.unpack_from
        _st_bin_resp_pack = RmiProtocol.ST_RESP.pack
        _BIN_REQ_SZ_L = RmiProtocol.REQ_SZ
        _BIN_RESP_NONE = RmiProtocol.RESP_NONE
        _BIN_RESP_OK = RmiProtocol.RESP_OK
        _BIN_RESP_ERR = RmiProtocol.RESP_ERR
        # M-08: struct fast-path 로컬 바인딩
        _BIN_RESP_INT = RmiProtocol.RESP_INT
        _BIN_RESP_FLOAT = RmiProtocol.RESP_FLOAT
        _BIN_RESP_TRUE = RmiProtocol.RESP_TRUE
        _BIN_RESP_FALSE = RmiProtocol.RESP_FALSE
        _BIN_RESP_STR = RmiProtocol.RESP_STR
        _st_i64_unpack = RmiProtocol.ST_I64.unpack_from
        _st_f64_unpack = RmiProtocol.ST_F64.unpack_from
        _st_i64x2_unpack = RmiProtocol.ST_I64x2.unpack_from
        _st_f64x2_unpack = RmiProtocol.ST_F64x2.unpack_from
        _st_u16_unpack = RmiProtocol.ST_U16.unpack_from
        _st_i64_pack = RmiProtocol.ST_I64.pack
        _st_f64_pack = RmiProtocol.ST_F64.pack
        _st_u16_pack = RmiProtocol.ST_U16.pack
        _MISSING_REF = _MISSING
        _method_cache = {}
        # ★ v2.6: Method Cache Pre-warm (첫 호출 getattr 제거)
        for _m_name in dir(job):
            if _m_name.startswith('_'): continue
            try:
                _m_ref = getattr(job, _m_name, _MISSING)
                if callable(_m_ref):
                    _method_cache[_m_name] = _m_ref
            except (RuntimeError, AttributeError):
                continue
        _callable = callable
        _getattr = getattr
        _setattr = setattr
        _vars = vars
        _isinstance = isinstance
        _type = type
        _time = time.time
        _queue_Empty = queue.Empty

        # ── 상수 + 로컬 바인딩 ──
        _RMI_BATCH = 5
        _BURST_LIMIT = 50            # v2.6: 카운트 기반 burst guard (perf_counter 제거)
        _NOTIFY_TIMEOUT = 0.1         # 100ms
        _STOP_CHECK_LIMIT = 100       # cycles
        _QUOTAS = scheduler.CYCLE_QUOTAS  # C8: 속성 조회 제거
        _sched_consume = scheduler.consume
        _sc_dispatch = sc._dispatch

        # ── 상태 ──
        _stop_local = False
        _cycle_count = 0
        _HB_CYCLES = 500            # v2.6: 카운트 기반 하트비트 (~500 cycles ≈ 1초)
        _hb_cnt = 0
        # M-10: IPC/Signal 비동시 실행 감지 (C4)
        _concurrent_warned = False

        def _check_stop():
            nonlocal _stop_local
            try: _stop_local = stop.value
            except Exception: _stop_local = True

        while self._run and not _stop_local:
          try:
            any_work = False
            _sig_active = False   # M-10
            _rmi_active = False   # M-10

            # ★ v2.6: 하트비트 갱신 (카운트 기반 — perf_counter 제거)
            if broker is not None:  # C7: broker=None 시 스킵
                _hb_cnt += 1
                if _hb_cnt >= _HB_CYCLES:
                    _hb_cnt = 0
                    try: broker._heartbeats[task_id] = _time()
                    except Exception: pass

            # ═══════════════════════════════════════════════
            # ① Signal RECV — QoS P1→P5 (★ 우선)
            #    v2.6: 카운트 기반 burst guard (perf_counter 제거)
            # ═══════════════════════════════════════════════
            if sc._qos_enabled:
                sc._cycle_time = _time()  # v2.6: cycle-time (QoS 활성 시만)
            scheduler.new_cycle()
            _burst_count = 0
            for p in range(5):
                quota = _QUOTAS[p]
                for _ in range(quota):
                    try:
                        data = qs[p].get_nowait()
                    except (_queue_Empty, IndexError):
                        break
                    _sc_dispatch(data)
                    _sched_consume(p)
                    any_work = True
                    _sig_active = True  # M-10
                    _burst_count += 1
                    if _burst_count >= _BURST_LIMIT:
                        break
                if _burst_count >= _BURST_LIMIT:
                    break

            # ═══════════════════════════════════════════════
            # ② IPC/RMI — request_q (Signal 후순)
            # ═══════════════════════════════════════════════
            for _ in range(_RMI_BATCH):
                data = _rb_read()
                if data is None:
                    break

                # ★ Phase 1: Binary/Pickle 자동 감지 (first byte)
                _is_bin = (data[0] == _BIN_REQ_L)
                if _is_bin:
                    _, _req_id, _flags, _mlen, _clen = _st_bin_req_unpack(data, 0)
                    _off = _BIN_REQ_SZ_L
                    m = data[_off:_off + _mlen].decode()
                    _off += _mlen
                    caller_id = data[_off:_off + _clen].decode() if _clen else None
                    _off += _clen
                    if _flags == 0x01:
                        _tail = _pickle_loads(data[_off:])
                        a = _tail.get('a', ())
                        kw = _tail.get('kw', {})
                        chain = _tail.get('chain', set())
                        rq = _tail.get('rq')
                    elif _flags == 0x02:
                        # ★ F04: binary chain decoding (pickle-free)
                        _n = data[_off]; _off += 1
                        chain = set()
                        for _ in range(_n):
                            _cl = data[_off]; _off += 1
                            chain.add(data[_off:_off + _cl].decode())
                            _off += _cl
                        a, kw, rq = (), {}, None
                    # M-08: struct fast-path request flags
                    elif _flags == 0x10:
                        a = (_st_i64_unpack(data, _off)[0],)
                        kw, chain, rq = {}, set(), None
                    elif _flags == 0x11:
                        a = (_st_f64_unpack(data, _off)[0],)
                        kw, chain, rq = {}, set(), None
                    elif _flags == 0x12:
                        a = _st_i64x2_unpack(data, _off)
                        kw, chain, rq = {}, set(), None
                    elif _flags == 0x13:
                        a = _st_f64x2_unpack(data, _off)
                        kw, chain, rq = {}, set(), None
                    elif _flags == 0x14:
                        _slen = _st_u16_unpack(data, _off)[0]
                        a = (data[_off + 2:_off + 2 + _slen].decode(),)
                        kw, chain, rq = {}, set(), None
                    else:
                        a, kw, chain, rq = (), {}, set(), None
                else:
                    req = _pickle_loads(data)
                    m, a, kw = req['m'], req.get('a', ()), req.get('kw', {})
                    _req_id = req.get('_id')
                    caller_id = req.get('c')
                    chain = req.get('chain', set())
                    rq = req.get('rq')

                _caller_resp_evt = None
                if caller_id:
                    caller_ti = _tasks_get(caller_id)
                    if caller_ti:
                        rq = caller_ti.response_q
                        _caller_resp_evt = caller_ti.response_event  # v2.6

                # ★ 순환 호출 감지
                if task_id in chain:
                    if rq:
                        try:
                            if _is_bin:
                                _err = {'e': "Circular RMI"}
                                rq.write(_st_bin_resp_pack(_BIN_RESP_L, _req_id, _BIN_RESP_ERR) + _pickle_dumps(_err, protocol=5))
                            else:
                                rq.write(_pickle_dumps({'e': "Circular RMI"}, protocol=5))
                        except Exception: pass
                        if _caller_resp_evt is not None:
                            try: _caller_resp_evt.set()
                            except Exception: pass
                    continue

                # Method Dispatch (기존 _rmi_handler 로직 동일)
                # ★ v2.6: chain을 thread-local에 저장 → 메서드 내 RMI 호출 시 자동 전파
                _rmi_chain_local.chain = chain | {task_id} if chain else {task_id}
                try:
                    if m[0] != '_':
                        fn = _method_cache.get(m)
                        if fn is None:
                            fn = _getattr(job, m, _MISSING_REF)
                            if fn is not _MISSING_REF:
                                _method_cache[m] = fn
                        if fn is _MISSING_REF:
                            res = {'e': f"Method '{m}' not found"}
                        else:
                            res = {'r': fn(*a, **kw) if _callable(fn) else fn}
                    elif m == '__getvar__':
                        val = _getattr(job, a[0], _MISSING_REF)
                        res = {'e': "Attr not found"} if val is _MISSING_REF else {'r': val() if _callable(val) else val}
                    elif m == '__setvar__':
                        _setattr(job, a[0], a[1]); res = {'r': True}
                    elif m == '__set_config__':
                        _setattr(job, a[0], a[1]); res = {'r': True}
                    elif m == '__get_debug__':
                        res = {'r': {'msg': self._debug_msg, 'signal': self._debug_signal, 'method': self._debug_method, 'variable': self._debug_variable, 'task': self._debug_task}}
                    elif m == '__set_debug__':
                        flag, value = a[0], bool(a[1])
                        if flag == 'msg': self._debug_msg = value
                        elif flag == 'signal': self._debug_signal = value
                        elif flag == 'method': self._debug_method = value
                        elif flag == 'variable': self._debug_variable = value
                        elif flag == 'task': self._debug_task = value
                        res = {'r': True}
                    elif m == '__list_vars__':
                        _SYS = {'running', 'task_name', 'runtime', 'signal'}
                        result = []
                        for name in sorted(_vars(job)):
                            if name.startswith('_') or name in _SYS: continue
                            val = _getattr(job, name, None)
                            if _callable(val): continue
                            vtype = _type(val).__name__
                            editable = _isinstance(val, (int, float, str, bool))
                            result.append({'name': name, 'type': vtype, 'value': val if editable else f"<{vtype}>", 'editable': editable})
                        res = {'r': result}
                    else:
                        res = {'e': "Unknown internal method"}
                except Exception as e:
                    res = {'e': str(e), 'trace': traceback.format_exc()}
                finally:
                    _rmi_chain_local.chain = None

                # Response 전송 (Phase 1: Binary/Pickle 자동 선택)
                if rq:
                    try:
                        if hasattr(rq, 'write'):
                            if _is_bin:
                                _e = res.get('e')
                                if _e is not None:
                                    resp_data = _st_bin_resp_pack(_BIN_RESP_L, _req_id, _BIN_RESP_ERR) + _pickle_dumps(res, protocol=5)
                                else:
                                    _r = res.get('r')
                                    if _r is None:
                                        resp_data = _st_bin_resp_pack(_BIN_RESP_L, _req_id, _BIN_RESP_NONE)
                                    # M-08: struct fast-path response
                                    elif _r is True:
                                        resp_data = _st_bin_resp_pack(_BIN_RESP_L, _req_id, _BIN_RESP_TRUE)
                                    elif _r is False:
                                        resp_data = _st_bin_resp_pack(_BIN_RESP_L, _req_id, _BIN_RESP_FALSE)
                                    elif _type(_r) is int:
                                        resp_data = _st_bin_resp_pack(_BIN_RESP_L, _req_id, _BIN_RESP_INT) + _st_i64_pack(_r)
                                    elif _type(_r) is float:
                                        resp_data = _st_bin_resp_pack(_BIN_RESP_L, _req_id, _BIN_RESP_FLOAT) + _st_f64_pack(_r)
                                    elif _type(_r) is str:
                                        _rb = _r.encode()
                                        resp_data = _st_bin_resp_pack(_BIN_RESP_L, _req_id, _BIN_RESP_STR) + _st_u16_pack(len(_rb)) + _rb
                                    else:
                                        resp_data = _st_bin_resp_pack(_BIN_RESP_L, _req_id, _BIN_RESP_OK) + _pickle_dumps(_r, protocol=5)
                            else:
                                if _req_id is not None:
                                    res['_id'] = _req_id
                                resp_data = _pickle_dumps(res, protocol=5)
                            for _ in range(_MAX_WRITE_RETRY):
                                if rq.write(resp_data):
                                    break
                                time.sleep(0)
                        else:
                            if not _is_bin and _req_id is not None:
                                res['_id'] = _req_id
                            rq.put(res)
                    except Exception: pass
                    # ★ v2.6: Caller의 response_event 깨움 (spin-wait → kernel wait)
                    if _caller_resp_evt is not None:
                        try: _caller_resp_evt.set()
                        except Exception: pass

                any_work = True
                _rmi_active = True  # M-10

            # M-10: IPC/Signal 비동시 실행 경고 (C4)
            if _sig_active and _rmi_active and not _concurrent_warned:
                _concurrent_warned = True
                _warnings.warn(
                    f"[{task_id}] IPC/Signal concurrent activity detected. "
                    f"C4 constraint: IPC and Signal should not run simultaneously.",
                    RuntimeWarning, stacklevel=1
                )

            # ═══════════════════════════════════════════════
            # ③ Kernel Wait — idle 시 CPU 0%
            #    v2.6: is_sleeping 플래그로 조건부 notify 지원
            # ═══════════════════════════════════════════════
            if not any_work:
                # ★ 잠들기 전 플래그 설정 (송신측이 읽고 조건부 set)
                if _is_sleeping is not None:
                    try: _is_sleeping.value = 1
                    except Exception: pass
                if notify_event is not None:
                    try: notify_event.clear()
                    except Exception: pass

                # Double-Check (Race Condition 방지)
                still_idle = True
                for p in range(5):
                    if not qs[p].empty():
                        still_idle = False; break
                if still_idle and _rb_used() > 0:
                    still_idle = False

                if still_idle:
                    if notify_event is not None:
                        try: notify_event.wait(timeout=_NOTIFY_TIMEOUT)
                        except Exception: time.sleep(0.001)
                    else:
                        time.sleep(0.001)
                # ★ 깨어남 — 플래그 해제
                if _is_sleeping is not None:
                    try: _is_sleeping.value = 0
                    except Exception: pass

            # Stop flag 주기적 확인
            _cycle_count += 1
            if _cycle_count >= _STOP_CHECK_LIMIT:
                _check_stop()
                _cycle_count = 0
          except Exception as _loop_err:
            print(f"[E:{task_id}] InvokeDispenser loop error: {_loop_err}\n{traceback.format_exc()}")
            time.sleep(0.01)  # 무한 에러 루프 방지


    def _inject_dependencies(self):
        job, tasks, ti = self.job, self.tasks, self.ti
        SmBlockHandler.injection(self, tasks)
        for k, v in ti.injection.items():
            if k == 'debug': continue  # config.json debug → TaskRuntime에서 처리
            if isinstance(v, dict) and "_smblock" in v: continue
            if isinstance(v, str) and v.startswith("client:"):
                # client:task_id → RmiClient 프록시 주입
                target_id = v[7:]
                t = tasks.get(target_id)
                if t:
                    setattr(job, k, self._create_rmi_client(target_id, t))
            elif isinstance(v, str) and v.startswith("task:"):
                # task:task_id → 실제 인스턴스 주입 (thread 모드 전용)
                target_id = v[5:]
                t = tasks.get(target_id)
                if t and t.job_instance:
                    setattr(job, k, t.job_instance)
                elif t:
                    # job_instance 없으면 RmiClient로 fallback
                    print(f"[W:{ti.task_id}] task:{target_id} has no instance, using RmiClient")
                    setattr(job, k, self._create_rmi_client(target_id, t))
            else:
                setattr(job, k, v)

    def _run_main_loop(self):
        ti, job = self.ti, self.job

        # @rmi_run 데코레이터 탐지
        loop_method = None
        try:
            for name in dir(job):
                if name.startswith('__'): continue
                try:
                    method = getattr(job, name, None)
                    if callable(method) and has_run_flag(method):
                        loop_method = method
                        break
                except (RuntimeError, AttributeError):
                    continue
        except (RuntimeError, AttributeError):
            pass

        # Fallback: run() 또는 rmi_loop()
        if loop_method is None:
            loop_method = getattr(job, 'run', None) or getattr(job, 'rmi_loop', None)

        if not loop_method:
            # QWidget/QObject (main_thread=True)는 run() 없이도 시그널 수신 대기
            if ti.main_thread:
                while self._run and not ti.stop_flag.value:
                    time.sleep(0.1)
                return
            return
        if self._debug_task:
            print(f"[{ti.task_id:>12s}] life | lifecycle(\"started\", mode={ti.mode}, pid={_os.getpid()})")
        while self._run:
            try:
                loop_method()  # run(self) - self.runtime으로 접근
                # main_thread=True (QWidget 등)는 run() 완료 후에도 시그널 수신 대기
                if ti.main_thread:
                    while self._run and not ti.stop_flag.value:
                        time.sleep(0.1)
                break
            except Exception as e:
                if self._debug_task:
                    print(f"[{ti.task_id:>12s}] life | lifecycle(\"error\", {repr(str(e))})")
                print(f"[W:{ti.task_id}] {loop_method.__name__}: {e}")
                traceback.print_exc()
                if not ti.auto_restart: break
                ti.restart_cnt.value += 1
                if self._debug_task:
                    print(f"[{ti.task_id:>12s}] life | lifecycle(\"restarting\", delay={ti.restart_delay})")
                time.sleep(ti.restart_delay)
        if self._debug_task:
            print(f"[{ti.task_id:>12s}] life | lifecycle(\"stopped\")")

    def should_stop(self) -> bool:
        try:
            return not self._run or self.ti.stop_flag.value
        except Exception:
            return True # Manager가 닫혔거나 통신 오류 시 정지 유도

    @property
    def running(self) -> bool:
        """Task가 실행 중인지 확인 (should_stop()의 반대)

        Example:
            # Before
            while not self.runtime.should_stop():
                pass

            # After (더 자연스러움)
            while self.running:
                pass
        """
        return not self.should_stop()

    @property
    def name(self) -> str:
        return self.ti.task_id

    def _create_rmi_client(self, task_id: str, target_info: TaskInfo):
        """RMI 클라이언트 생성 헬퍼
        - Thread-Thread: DirectClient (IPC 없음, 최고 성능)
        - 그 외: RmiClient + Queue (단일 채널)
        """
        ti = self.ti
        caller_mode = ti.mode
        target_mode = target_info.mode

        # Thread-Thread: 직접 호출 (IPC 없음)
        if caller_mode == 'thread' and target_mode == 'thread':
            tasks = self.tasks
            def job_getter():
                # Lazy loading: target task의 job 인스턴스 반환
                # thread 모드에서는 같은 프로세스이므로 직접 접근 가능
                target_ti = tasks.get(task_id)
                if target_ti and target_ti.job_instance:
                    return target_ti.job_instance
                # job_instance가 없으면 runtimes에서 찾기 시도
                return None
            return DirectClient(task_id, job_getter, caller=ti.task_id,
                                debug_method=self._debug_method,
                                debug_variable=self._debug_variable,
                                broker=self._signal_broker)

        # ★ 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)
            if _ne is None and _IS_WIN32:
                ne_name = self._signal_broker._notify_event_names.get(task_id)
                if ne_name:
                    try:
                        from ..sm_infra import SmKernelEvent
                        _ne = SmKernelEvent(ne_name, create=False, manual_reset=True)
                    except Exception:
                        _ne = None

        # SmRingBuffer 단일 채널 (Lock-free SPSC)
        return RmiClient(
            task_id, target_info.request_q, ti.response_q, timeout=self._rmi_timeout,
            request_lock=target_info.request_lock,
            tx_counter=ti.rmi_tx, time_counter=ti.rmi_time,
            time_min=ti.rmi_time_min, time_max=ti.rmi_time_max,
            caller=ti.task_id,
            debug_method=self._debug_method,
            debug_variable=self._debug_variable,
            broker=self._signal_broker,
            notify_event=_ne,
            response_event=ti.response_event,               # v2.6: caller의 응답 이벤트
            target_sleeping=target_info.is_sleeping          # v2.6: target의 is_sleeping 플래그
        )

    def get_client(self, task_id: str) -> RmiClient:
        t = self.tasks.get(task_id)
        if not t: raise ValueError(f"Task '{task_id}' not found")
        return self._create_rmi_client(task_id, t)

    # ★ Phase 4: TaskRuntime.restart_task() 제거 (dead code — __slots__ 불일치로 호출 시 AttributeError)

    @property
    def signal(self) -> SignalClient:
        return self._signal_client
