"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASK v2.0 Project                                                          ║
║  Task Manager - Multiprocess/Thread Management System                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.0.0                                                             ║
║  Date    : 2026-01-30                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Classes:                                                                    ║
║    - TaskClass    : Decorator for task class registration                    ║
║    - TaskInfo     : Dataclass holding task metadata and IPC resources        ║
║    - TaskManager  : Main manager for task lifecycle and monitoring           ║
║    - TaskWorker   : Worker wrapper executing task loop and RMI handler       ║
║    - RmiClient    : Client proxy for remote method invocation                ║
║    - _RmiProxy    : Lazy proxy for transparent remote variable access        ║
║                                                                              ║
║  Functions:                                                                  ║
║    - _check_port_in_use : Check if a port is already bound                   ║
║    - _check_singleton   : Ensure single instance via port binding            ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

from __future__ import annotations
from typing import Any, Dict, Optional
from dataclasses import dataclass, field
import threading
import multiprocessing
import time
import socket
import traceback
import queue
import atexit
import signal

from .task_performance import TimingRecorder, PerformanceCollector

_task_registry: Dict[str, type] = {}
_MISSING = object()  # sentinel for getattr


def _check_port_in_use(port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        try:
            s.bind(('0.0.0.0', port))
            return False
        except OSError:
            return True


def _check_singleton(port: int) -> bool:
    if _check_port_in_use(port):
        print(f"\n{'='*60}\n[ERROR] 포트 {port} 사용 중 - 다른 인스턴스 실행 중\n{'='*60}\n")
        return False
    return True


class TaskClass:
    __slots__ = ('name', 'mode', 'restart', 'restart_delay')

    def __init__(self, name: str, mode: str = "thread", restart: bool = True, restart_delay: float = 3.0):
        self.name, self.mode, self.restart, self.restart_delay = name, mode, restart, restart_delay

    def __call__(self, cls):
        _task_registry[self.name] = cls
        cls._tc_name, cls._tc_mode = self.name, self.mode
        cls._tc_restart, cls._tc_delay = self.restart, self.restart_delay
        return cls

    @staticmethod
    def get(name: str) -> Optional[type]: return _task_registry.get(name)

    @staticmethod
    def attr(name: str, key: str, default=None):
        c = _task_registry.get(name)
        return getattr(c, key, default) if c else default


@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
    request_q: Any = None
    response_q: Any = None
    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  # Manager.dict() for per-method call counts
    rmi_timing: Any = None   # Manager.dict() for per-method timing (10-window: [ipc_times], [func_times])

    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 = TaskClass.get(n)
            self.mode = TaskClass.attr(n, '_tc_mode', 'thread')
            self.auto_restart = TaskClass.attr(n, '_tc_restart', True)
            self.restart_delay = TaskClass.attr(n, '_tc_delay', 3.0)


class _RmiProxy:
    __slots__ = ('_client', '_name', '_value', '_resolved')

    def __init__(self, client: 'RmiClient', name: str):
        # 단일 할당으로 최적화
        sa = object.__setattr__
        sa(self, '_client', client)
        sa(self, '_name', name)
        sa(self, '_value', None)
        sa(self, '_resolved', False)

    def _resolve(self):
        if not self._resolved:
            sa = object.__setattr__
            sa(self, '_value', self._client._call('__getvar__', self._name))
            sa(self, '_resolved', True)
        return self._value

    def __call__(self, *a, **kw): return self._client._call(self._name, *a, **kw)

    # 비교 연산자
    def __eq__(self, o): return self._resolve() == o
    def __ne__(self, o): return self._resolve() != o
    def __lt__(self, o): return self._resolve() < o
    def __le__(self, o): return self._resolve() <= o
    def __gt__(self, o): return self._resolve() > o
    def __ge__(self, o): return self._resolve() >= o

    # 산술 연산자
    def __add__(self, o): return self._resolve() + o
    def __radd__(self, o): return o + self._resolve()
    def __sub__(self, o): return self._resolve() - o
    def __rsub__(self, o): return o - self._resolve()
    def __mul__(self, o): return self._resolve() * o
    def __rmul__(self, o): return o * self._resolve()
    def __truediv__(self, o): return self._resolve() / o
    def __floordiv__(self, o): return self._resolve() // o
    def __mod__(self, o): return self._resolve() % o

    # 단항/변환
    def __neg__(self): return -self._resolve()
    def __pos__(self): return +self._resolve()
    def __abs__(self): return abs(self._resolve())
    def __int__(self): return int(self._resolve())
    def __float__(self): return float(self._resolve())
    def __str__(self): return str(self._resolve())
    def __bool__(self): return bool(self._resolve())
    def __repr__(self): return f"_RmiProxy({self._name})"

    # 컨테이너
    def __len__(self): return len(self._resolve())
    def __getitem__(self, k): return self._resolve()[k]
    def __contains__(self, i): return i in self._resolve()
    def __iter__(self): return iter(self._resolve())


class RmiClient:
    __slots__ = ('_id', '_req_q', '_res_q', '_timeout', '_tx_cnt', '_time_cnt', '_time_min', '_time_max')

    def __init__(self, target_id: str, request_q, response_q, timeout: float = 3.0,
                 tx_counter=None, time_counter=None, time_min=None, time_max=None):
        sa = object.__setattr__
        sa(self, '_id', target_id)
        sa(self, '_req_q', request_q)
        sa(self, '_res_q', response_q)
        sa(self, '_timeout', timeout)
        sa(self, '_tx_cnt', tx_counter)
        sa(self, '_time_cnt', time_counter)
        sa(self, '_time_min', time_min)
        sa(self, '_time_max', time_max)

    def _call(self, m: str, *a, **kw):
        tx = self._tx_cnt
        if tx: tx.value += 1
        t0 = time.time()
        self._req_q.put({'m': m, 'a': a, 'kw': kw, 'rq': self._res_q, 't0': t0})
        try:
            r = self._res_q.get(timeout=self._timeout)
            elapsed = (time.time() - t0) * 1000
            tc, tmin, tmax = self._time_cnt, self._time_min, self._time_max
            if tc: tc.value += elapsed
            if tmin and elapsed < tmin.value: tmin.value = elapsed
            if tmax and elapsed > tmax.value: tmax.value = elapsed
            if r.get('e'):
                err_msg = r['e']
                if r.get('trace'):
                    err_msg += f"\n--- Remote Traceback ---\n{r['trace']}"
                raise Exception(err_msg)
            return r.get('r')
        except queue.Empty:
            raise Exception(f"RmiClient({self._id}).{m}: Timeout after {self._timeout}s")
        except Exception as e:
            raise Exception(f"RmiClient({self._id}).{m}: {e}") from e

    def __getattr__(self, m: str):
        if m[0] == '_': raise AttributeError(m)
        return _RmiProxy(self, m)

    def __setattr__(self, m: str, value):
        if m[0] == '_':
            object.__setattr__(self, m, value)
        else:
            self._call('__setvar__', m, value)

    def __repr__(self): return f"RmiClient({self._id})"


class TaskManager:
    __slots__ = ('_tasks', '_workers', '_mgr', '_monitor_port', '_monitor', '_gconfig', '_exit_hook', '_measurement', '_performance')

    def __init__(self, gconfig):
        """Initialize TaskManager with GConfig instance.

        Args:
            gconfig: GConfig instance with task_config section containing:
                - task definitions: "task_id/class_name": {config...}
                - _monitor (optional): {"port": int, "exit_hook": bool}
                  - port: Required for monitor to start
                  - exit_hook: If True, stop_all() is called on process exit
        """
        self._tasks: Dict[str, TaskInfo] = {}
        self._workers: Dict[str, Any] = {}
        self._mgr = multiprocessing.Manager()
        self._gconfig = gconfig
        self._monitor = None
        self._exit_hook = False

        # Extract task_config from gconfig
        config = gconfig.data_get("task_config", {})

        # Handle _monitor settings
        monitor_cfg = 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
        # RMI method measurement (shared across processes)
        measurement_default = monitor_cfg.get("measurement", False) if monitor_cfg else False
        self._measurement = self._mgr.Value('b', measurement_default)

        mgr = self._mgr

        for key, cfg in config.items():
            # Skip keys starting with '_' (like _monitor)
            if key.startswith("_"):
                continue
            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)
            ti.request_q, ti.response_q = mgr.Queue(), mgr.Queue()
            ti.stop_flag = mgr.Value('b', False)
            ti.rmi_rx, ti.rmi_tx, ti.rmi_fail = mgr.Value('i', 0), mgr.Value('i', 0), mgr.Value('i', 0)
            ti.rmi_time, ti.rmi_time_min, ti.rmi_time_max = mgr.Value('d', 0.0), mgr.Value('d', float('inf')), mgr.Value('d', 0.0)
            ti.restart_cnt = mgr.Value('i', 0)
            ti.rmi_methods = mgr.dict()  # Per-method call counts
            ti.rmi_timing = mgr.dict()   # Per-method timing (10-window)
            self._tasks[tid] = ti

        # PerformanceCollector 생성 (성능 통계 수집용)
        self._performance = PerformanceCollector(self._tasks, self._workers, self._measurement)

    @property
    def performance(self) -> PerformanceCollector:
        """성능 데이터 수집기 접근"""
        return self._performance

    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")

        self._print()

        # SmBlock 생성 (_smblock 설정이 있으면)
        SmBlockHandler.creation(self._gconfig, self._mgr, self._tasks)

        for tid, ti in self._tasks.items():
            if tid not in self._workers:
                w = TaskWorker(ti, self._tasks, measurement=self._measurement)
                h = multiprocessing.Process(target=w.run, daemon=True) if ti.mode == 'process' else threading.Thread(target=w.run, daemon=True)
                h.start()
                self._workers[tid] = h
                print(f"[Manager] {tid} started ({ti.mode})")

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

        # Register exit handler if exit_hook is enabled
        if self._exit_hook:
            atexit.register(self.stop_all)
            # signal.signal can only be called from main thread
            if threading.current_thread() is threading.main_thread():
                signal.signal(signal.SIGINT, lambda s, f: self.stop_all())
                print("[Manager] exit_hook registered (atexit + SIGINT)")
            else:
                print("[Manager] exit_hook registered (atexit only, non-main thread)")

    def stop_all(self, skip_monitor: bool = False):
        print(f"[Manager:stop_all] called, tasks={list(self._tasks.keys())}, skip_monitor={skip_monitor}")
        if self._monitor and not skip_monitor:
            print(f"[Manager:stop_all] stopping monitor")
            self._monitor.stop()
            self._monitor = None
        for tid, ti in self._tasks.items():
            print(f"[Manager:stop_all] setting stop_flag for {tid}")
            ti.stop_flag.value = True
        for tid, w in self._workers.items():
            print(f"[Manager:stop_all] joining {tid}, alive={w.is_alive()}")
            w.join(timeout=4.0)  # RMI timeout(3s) + margin
            if w.is_alive():
                print(f"[Manager:stop_all] {tid} still alive after timeout, terminating...")
                if hasattr(w, 'terminate'):
                    w.terminate()
            print(f"[Manager:stop_all] {tid} stopped")
        self._workers.clear()

        # SmBlock 정리
        SmBlockHandler.close_all()
        print(f"[Manager:stop_all] completed")

    def clear_stats(self):
        inf = float('inf')
        for ti in self._tasks.values():
            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()  # Clear per-method counts
            ti.rmi_timing.clear()   # Clear per-method timing

    def get_status(self) -> Dict[str, Dict[str, Any]]:
        result = {}
        inf = float('inf')
        for tid, ti in self._tasks.items():
            w = self._workers.get(tid)
            tx = ti.rmi_tx.value
            tt = ti.rmi_time.value
            tmin = 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': ti.rmi_rx.value, 'rmi_tx': tx, 'rmi_fail': ti.rmi_fail.value,
                'rmi_avg': tt / tx if tx else 0.0,
                'rmi_min': 0.0 if tmin == inf else tmin,
                'rmi_max': ti.rmi_time_max.value,
                'rxq_size': ti.request_q.qsize(),
                'txq_size': ti.response_q.qsize(),
                'restart': ti.restart_cnt.value,
                'rmi_methods': dict(ti.rmi_methods),  # Per-method call counts
            }
        return result

    def _print(self):
        print("\n" + "=" * 60)
        print(f"{'ID':<15} {'Class':<12} {'Mode':<8} {'Job':<15}")
        print("-" * 60)
        for tid, ti in self._tasks.items():
            print(f"{tid:<15} {ti.task_class_name:<12} {ti.mode:<8} {ti.job_class.__name__ if ti.job_class else '-':<15}")
        print("=" * 60 + "\n")

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


class TaskWorker:
    __slots__ = ('ti', 'tasks', 'job', '_run', '_th', '_measurement', '_timing_recorder')

    def __init__(self, ti: TaskInfo, tasks: Dict[str, TaskInfo], measurement=None):
        self.ti, self.tasks, self.job, self._run, self._th = ti, tasks, None, True, None
        self._measurement = measurement  # Shared measurement flag
        self._timing_recorder = TimingRecorder(ti.rmi_methods, ti.rmi_timing)

    def run(self):
        ti = self.ti
        if not ti.job_class:
            print(f"[W:{ti.task_id}] no job_class"); return
        self.job = ti.job_class()
        self.job.task = self
        self._th = threading.Thread(target=self._rmi_handler, daemon=True)
        self._th.start()
        self._inject()
        print(f"[W:{ti.task_id}] started")
        self._loop()

    def _rmi_handler(self):
        ti = self.ti
        q, job = ti.request_q, self.job
        stop = ti.stop_flag
        rx, fail = ti.rmi_rx, ti.rmi_fail
        recorder = self._timing_recorder

        while self._run and not stop.value:
            try:
                req = q.get(timeout=0.5)
            except queue.Empty:
                # Flush on idle
                if self._measurement and self._measurement.value:
                    recorder.flush()
                continue
            except Exception as e:
                print(f"[W:{ti.task_id}] Queue error at {__file__}: {e}")
                continue
            if stop.value: break

            t_recv = time.time()  # Time when request was received
            rx.value += 1
            m, a, kw, rq = req['m'], req.get('a', ()), req.get('kw', {}), req.get('rq')
            t0 = req.get('t0')  # Time when request was sent (for IPC measurement)

            # 측정 여부 결정 (내부 메서드 '_' 제외)
            measure = m[0] != '_' and self._measurement and self._measurement.value

            t_func_start = time.time()  # Start of function execution
            try:
                # 빠른 경로: 일반 메서드 호출 (가장 빈번)
                if m[0] != '_':
                    fn = getattr(job, m, _MISSING)
                    if fn is _MISSING:
                        res = {'e': f"'{m}' not found"}
                        fail.value += 1
                    else:
                        res = {'r': fn(*a, **kw) if callable(fn) else fn}
                # 특수 메서드 디스패치
                elif m == '__getvar__':
                    val = getattr(job, a[0], _MISSING)
                    if val is _MISSING:
                        res = {'e': f"'{a[0]}' not found"}
                        fail.value += 1
                    else:
                        res = {'r': val() if callable(val) else val}
                elif m == '__setvar__':
                    setattr(job, a[0], a[1])
                    res = {'r': True}
                elif m == '__set_config__':
                    print(f"[W:{ti.task_id}:RMI] __set_config__ key={a[0]}, value={a[1]}, type={type(a[1])}")
                    before_val = getattr(job, a[0], _MISSING)
                    print(f"[W:{ti.task_id}:RMI] before setattr: {a[0]}={before_val}")
                    setattr(job, a[0], a[1])
                    after_val = getattr(job, a[0], _MISSING)
                    print(f"[W:{ti.task_id}:RMI] after setattr: {a[0]}={after_val}")
                    res = {'r': True}
                elif m == '__get_config__':
                    res = {'r': {k: getattr(job, k, None) for k in (a[0] if a else [])}}
                else:
                    res = {'e': f"Unknown method '{m}'"}
                    fail.value += 1
            except Exception as e:
                res = {'e': str(e), 'trace': traceback.format_exc()}
                fail.value += 1
                print(f"[W:{ti.task_id}] RMI error in '{m}': {e}")

            # Record timing (IPC + FUNC) via TimingRecorder
            if measure:
                t_func_end = time.time()
                if t0:
                    ipc_ms = (t_recv - t0) * 1000  # IPC time in ms
                    func_ms = (t_func_end - t_func_start) * 1000  # FUNC time in ms
                    recorder.record(m, ipc_ms, func_ms)
                else:
                    recorder.record_count_only(m)

            if rq:
                try:
                    rq.put(res)
                except Exception as e:
                    print(f"[W:{ti.task_id}] Response queue error: {e}")

        # Final flush on exit
        recorder.flush()

    def _inject(self):
        job, tasks, ti = self.job, self.tasks, self.ti
        SmBlockHandler.injection(self, tasks)
        for k, v in ti.injection.items():
            # smblock 설정은 SmBlockHandler에서 처리됨
            if isinstance(v, dict) and "_smblock" in v:
                continue
            if isinstance(v, str) and v[:5] == "task:":
                t = tasks.get(v[5:])
                if t:
                    setattr(job, k, RmiClient(
                        v[5:], t.request_q, ti.response_q,
                        tx_counter=ti.rmi_tx, time_counter=ti.rmi_time,
                        time_min=ti.rmi_time_min, time_max=ti.rmi_time_max
                    ))
            else:
                setattr(job, k, v)

    def _loop(self):
        ti, job = self.ti, self.job
        task_loop = getattr(job, 'task_loop', None)
        if not task_loop: return

        while self._run:
            try:
                task_loop()
                break
            except Exception as e:
                print(f"[W:{ti.task_id}] {e}")
                if not ti.auto_restart: break
                ti.restart_cnt.value += 1
                time.sleep(ti.restart_delay)

    def should_stop(self) -> bool: return not self._run or self.ti.stop_flag.value

    @property
    def name(self) -> str: return self.ti.task_id
    
class SmBlockHandler:
    """SmBlock 공유메모리 블록 할당/주입 핸들러.

    Config format example:
        task_config:
            _smblock:
                image_pool: {shape: [480, 640, 3], maxsize: 100}
            task_id/class_name:
                camera: "smblock:image_pool"  # Inject SmBlock reference

    동작 원리:
        1. creation(): _smblock 설정을 파싱하여 SmBlock 생성
           - "smblock:name" 문자열을 dict로 변환하여 injection에 저장
           - dict에는 name, shape, maxsize, lock 포함 (프로세스 간 공유)
        2. injection(): 워커에서 dict를 읽어 SmBlock 인스턴스 생성/연결
           - thread 모드: _pools에서 직접 참조
           - process 모드: injection dict의 설정으로 새로 attach
    """
    _pools: Dict[str, Any] = {}  # name -> SmBlock instance (메인 프로세스용)

    @classmethod
    def creation(cls, gconfig, mgr, tasks: Dict[str, TaskInfo]) -> Dict[str, Any]:
        """_smblock 설정에서 SmBlock 인스턴스 생성 및 injection 설정 변환.

        Args:
            gconfig: GConfig 인스턴스 (task_config._smblock 설정 포함)
            mgr: multiprocessing.Manager 인스턴스 (Lock 생성용)
            tasks: TaskInfo 딕셔너리 (injection 설정 변환용)

        Returns:
            Dict[str, SmBlock]: name -> SmBlock 인스턴스 매핑
        """
        from .SmBlock import SmBlock

        config = gconfig.data_get("task_config", {})
        smblock_cfg = config.get("_smblock", {})

        if not smblock_cfg:
            return cls._pools

        # 1. SmBlock 생성 및 설정 저장
        smblock_info = {}  # name -> {shape, maxsize, lock}
        for name, cfg in smblock_cfg.items():
            if name in cls._pools:
                continue

            shape = cfg.get("shape")
            maxsize = cfg.get("maxsize", 100)

            if not shape:
                print(f"[SmBlockHandler] '{name}': shape 필수 설정 누락")
                continue

            shape = tuple(shape) if isinstance(shape, list) else shape
            lock = mgr.Lock()

            try:
                pool = SmBlock(name, shape, maxsize, create=True, lock=lock)
                cls._pools[name] = pool
                smblock_info[name] = {"name": name, "shape": shape, "maxsize": maxsize, "lock": lock}
                print(f"[SmBlockHandler] SmBlock '{name}' created: shape={shape}, maxsize={maxsize}")
            except FileExistsError:
                # 기존 공유 메모리가 남아있는 경우 - 정리 후 재생성
                from multiprocessing.shared_memory import SharedMemory
                try:
                    old_shm = SharedMemory(name=name, create=False)
                    old_shm.close()
                    old_shm.unlink()
                    print(f"[SmBlockHandler] SmBlock '{name}' old memory cleaned up")
                except Exception:
                    pass
                # 새로 생성
                pool = SmBlock(name, shape, maxsize, create=True, lock=lock)
                cls._pools[name] = pool
                smblock_info[name] = {"name": name, "shape": shape, "maxsize": maxsize, "lock": lock}
                print(f"[SmBlockHandler] SmBlock '{name}' recreated: shape={shape}, maxsize={maxsize}")
            except Exception as e:
                print(f"[SmBlockHandler] SmBlock '{name}' creation failed: {e}")

        # 2. 각 task의 injection에서 "smblock:xxx" 문자열을 dict로 변환
        for tid, ti in tasks.items():
            for k, v in list(ti.injection.items()):
                if isinstance(v, str) and v.startswith("smblock:"):
                    pool_name = v[8:]
                    if pool_name in smblock_info:
                        # 문자열 -> dict 변환 (Manager Lock 포함)
                        ti.injection[k] = {
                            "_smblock": pool_name,
                            **smblock_info[pool_name]
                        }
                        print(f"[SmBlockHandler] {tid}.{k}: injection converted to dict")
                    else:
                        # 정의되지 않은 smblock 참조 - 오류 발생
                        available = list(smblock_info.keys()) if smblock_info else "(none)"
                        raise ValueError(
                            f"[SmBlockHandler] {tid}.{k}: SmBlock '{pool_name}' not defined in _smblock. "
                            f"Available: {available}"
                        )

        return cls._pools

    @classmethod
    def injection(cls, worker: 'TaskWorker', _tasks: Dict[str, TaskInfo]):
        """smblock injection dict를 SmBlock 인스턴스로 변환하여 job에 주입.

        Args:
            worker: TaskWorker 인스턴스
            _tasks: 전체 태스크 정보 딕셔너리 (미사용, API 일관성 유지)
        """
        from .SmBlock import SmBlock

        job = worker.job
        ti = worker.ti

        for k, v in ti.injection.items():
            # dict 형태의 smblock 설정 처리
            if isinstance(v, dict) and "_smblock" in v:
                pool_name = v["_smblock"]
                shape = tuple(v["shape"])
                maxsize = v["maxsize"]
                lock = v["lock"]  # Manager Lock (프로세스 간 공유됨)

                # thread 모드: 메인 프로세스의 _pools에서 참조
                if pool_name in cls._pools:
                    setattr(job, k, cls._pools[pool_name])
                    print(f"[SmBlockHandler] {ti.task_id}.{k} <- SmBlock '{pool_name}' (direct)")
                else:
                    # process 모드: 새로 attach
                    try:
                        pool = SmBlock(pool_name, shape, maxsize, create=False, lock=lock)
                        setattr(job, k, pool)
                        print(f"[SmBlockHandler] {ti.task_id}.{k} <- SmBlock '{pool_name}' (attached)")
                    except Exception as e:
                        print(f"[SmBlockHandler] {ti.task_id}.{k}: SmBlock attach failed: {e}")

    @classmethod
    def close_all(cls):
        """모든 SmBlock 인스턴스 정리 (메인 프로세스에서만 호출)."""
        for name, pool in cls._pools.items():
            try:
                pool.close()
                print(f"[SmBlockHandler] SmBlock '{name}' closed")
            except Exception as e:
                print(f"[SmBlockHandler] SmBlock '{name}' close failed: {e}")
        cls._pools.clear()
