# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""RMI 프로토콜 + 클라이언트 + 프록시

RmiProtocol: Binary RMI Protocol v2 (struct fast-path + pickle fallback)
RmiClient, _RmiProxy: RMI 클라이언트 및 프록시
DirectClient: Thread 직접 호출 (프로세스 내 bypass)
"""

from __future__ import annotations
from typing import Any
import struct
import threading
import time
import pickle
import queue
from contextlib import nullcontext as _nullcontext

from ._comm_utils import _fmt_debug_call, _build_chain, _make_async_worker


# ==============================================================================
# RmiProtocol — Binary RMI Protocol v2 (M-08: struct fast-path)
# ==============================================================================

class RmiProtocol:
    """Binary RMI Protocol v2 (M-08: struct fast-path)

    Layout (Request):  [magic:u8] [seq:u16] [flags:u8] [mlen:u8] [clen:u8]
    Layout (Response): [magic:u8] [seq:u16] [status:u8]
    """
    BIN_REQ = 0xB1
    BIN_RESP = 0xB2

    # Response status
    RESP_NONE = 0    # result is None
    RESP_OK = 1      # pickled result
    RESP_ERR = 2     # pickled error dict
    # M-08: struct fast-path response
    RESP_INT = 3     # int64
    RESP_FLOAT = 4   # float64
    RESP_TRUE = 5    # True
    RESP_FALSE = 6   # False
    RESP_STR = 7     # UTF-8 string

    # M-08: Request flags (struct fast-path)
    FLAG_INT1 = 0x10         # (int,)
    FLAG_FLOAT1 = 0x11       # (float,)
    FLAG_INT2 = 0x12         # (int, int)
    FLAG_FLOAT2 = 0x13       # (float, float)
    FLAG_STR1 = 0x14         # (str,)

    ST_REQ = struct.Struct('<BHBBB')   # 6B: magic, seq, flags, mlen, clen
    ST_RESP = struct.Struct('<BHB')    # 4B: magic, seq, status
    REQ_SZ = ST_REQ.size               # 6
    RESP_SZ = ST_RESP.size             # 4

    # Struct fast-path formatters (M-08)
    ST_I64 = struct.Struct('<q')       # 8B int64
    ST_F64 = struct.Struct('<d')       # 8B float64
    ST_I64x2 = struct.Struct('<qq')    # 16B int64 × 2
    ST_F64x2 = struct.Struct('<dd')    # 16B float64 × 2
    ST_U16 = struct.Struct('<H')       # 2B uint16 (string length prefix)

    RESP_NONE_DICT = {'r': None}       # pre-built (heap 할당 제거)
    RESP_TRUE_DICT = {'r': True}
    RESP_FALSE_DICT = {'r': False}

    @staticmethod
    def unpack_response(data, seq):
        """Binary/Pickle RMI 응답 디코딩. stale이면 None 반환."""
        P = RmiProtocol
        if data[0] == P.BIN_RESP:
            _, _seq, _st = P.ST_RESP.unpack_from(data, 0)
            if _seq != seq:
                return None  # stale
            if _st == P.RESP_NONE:
                return P.RESP_NONE_DICT
            if _st == P.RESP_OK:
                return {'r': pickle.loads(data[P.RESP_SZ:])}
            # M-08: struct fast-path responses
            if _st == P.RESP_INT:
                return {'r': P.ST_I64.unpack_from(data, P.RESP_SZ)[0]}
            if _st == P.RESP_FLOAT:
                return {'r': P.ST_F64.unpack_from(data, P.RESP_SZ)[0]}
            if _st == P.RESP_TRUE:
                return P.RESP_TRUE_DICT
            if _st == P.RESP_FALSE:
                return P.RESP_FALSE_DICT
            if _st == P.RESP_STR:
                _slen = P.ST_U16.unpack_from(data, P.RESP_SZ)[0]
                return {'r': data[P.RESP_SZ + 2:P.RESP_SZ + 2 + _slen].decode()}
            return pickle.loads(data[P.RESP_SZ:])  # error dict or unknown
        r = pickle.loads(data)
        return r if r.get('_id', seq) == seq else None


# ==============================================================================
# RMI Client
# ==============================================================================
from .task_error import (
    RmiTimeoutError, InvokeDeadlockError, rmi_timeout_error,
)
from ._signal_core import PayloadTooLargeError  # F4

# ──────────────────────────────────────────────────────────────────────────────
# Constants (task_manager.py에서 이동)
# ──────────────────────────────────────────────────────────────────────────────
_MISSING = object()
_DEFAULT_RMI_TIMEOUT = 2.0
_MAX_WRITE_RETRY = 2000    # F3: burst 내성 강화 (~200ms)
_SPIN_CHECK = 200          # response spin-wait timeout 체크 주기

# ── RMI chain 전파용 thread-local (A↔B 상호 호출 교착 감지) ──
_rmi_chain_local = threading.local()


# ── UI 스레드 감지 (Qt 의존 없이 lazy) ──
class _QtUiDetector:
    """Qt UI 스레드 감지 (lazy 초기화, Qt 미설치 시 항상 False)"""
    __slots__ = ('_done', '_check_fn', 'available')

    def __init__(self):
        self._done = False
        self._check_fn = None
        self.available = False

    def is_ui_thread(self) -> bool:
        if not self._done:
            self._done = True
            try:
                from PySide6.QtWidgets import QApplication
                from PySide6.QtCore import QThread
                self._check_fn = lambda: (
                    (app := QApplication.instance()) is not None
                    and app.thread() == QThread.currentThread()
                )
                self.available = True
            except ImportError:
                try:
                    from PyQt6.QtWidgets import QApplication
                    from PyQt6.QtCore import QThread
                    self._check_fn = lambda: (
                        (app := QApplication.instance()) is not None
                        and app.thread() == QThread.currentThread()
                    )
                    self.available = True
                except ImportError:
                    self._check_fn = None
                    self.available = False
        return self._check_fn() if self._check_fn else False

_qt_ui = _QtUiDetector()


# ──────────────────────────────────────────────────────────────────────────────
# _ProxyOpsMixin - Shared operator mixin for proxy classes
# ──────────────────────────────────────────────────────────────────────────────

class _ProxyOpsMixin:
    """★ Phase 2: Proxy 공통 연산자 — _resolve() 기반 (중복 제거)"""
    __slots__ = ()
    def _resolve(self): raise NotImplementedError

    # 비교
    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 __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())


# ──────────────────────────────────────────────────────────────────────────────
# _RmiProxy - Lazy proxy for remote variable access
# ──────────────────────────────────────────────────────────────────────────────

class _RmiProxy(_ProxyOpsMixin):
    __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 nowait(self, *a, **kw):
        """비동기 RMI 호출 (Fire-and-Forget)"""
        return self._client._call_nowait(self._name, *a, **kw)

    def async_call(self, *a, done: str = None, error: str = None, **kw):
        """비동기 RMI 호출 + Signal 콜백 (UI 스레드 안전)"""
        return self._client._call_async(self._name, *a, done=done, error=error, **kw)

    def __repr__(self): return f"_RmiProxy({self._name})"


# ──────────────────────────────────────────────────────────────────────────────
# RmiClient - Remote Method Invocation Client
# ──────────────────────────────────────────────────────────────────────────────

class RmiClient:
    __slots__ = ('_id', '_req_q', '_req_lock', '_res_q', '_timeout',
                 '_tx_cnt', '_time_cnt', '_time_min', '_time_max', '_caller',
                 '_caller_enc',
                 '_debug_method', '_debug_variable', '_broker', '_notify_event',
                 '_response_event', '_target_sleeping', '_req_seq', '_resp_lock',
                 '_has_async')

    def __init__(self, target_id: str, request_q, response_q, timeout: float = _DEFAULT_RMI_TIMEOUT,
                 request_lock=None,
                 tx_counter=None, time_counter=None, time_min=None, time_max=None, caller: str = None,
                 debug_method=False, debug_variable=False, broker=None, notify_event=None,
                 response_event=None, target_sleeping=None):
        sa = object.__setattr__
        sa(self, '_id', target_id)
        sa(self, '_req_q', request_q)
        sa(self, '_req_lock', request_lock)
        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)
        sa(self, '_caller', caller)
        sa(self, '_caller_enc', caller.encode() if caller else b'')
        sa(self, '_debug_method', debug_method)
        sa(self, '_debug_variable', debug_variable)
        sa(self, '_broker', broker)
        sa(self, '_notify_event', notify_event)
        sa(self, '_response_event', response_event)
        sa(self, '_target_sleeping', target_sleeping)
        sa(self, '_req_seq', 0)
        sa(self, '_resp_lock', threading.Lock())
        sa(self, '_has_async', False)

    def _trace_call(self, m, a, kw, include_getvar=False):
        """RMI 호출 debug tracing (공통)."""
        is_var_access = m in ('__getvar__', '__setvar__')
        should_trace = self._debug_variable if is_var_access else self._debug_method
        if not should_trace:
            return is_var_access, False
        caller_str = self._caller or "(external)"
        if is_var_access:
            var_name = a[0] if a else ''
            if m == '__setvar__':
                val = a[1] if len(a) > 1 else None
                print(f"[{caller_str:>12s}] var  | set(\"{self._id}.{var_name}\", {repr(val)})")
            elif include_getvar:
                print(f"[{caller_str:>12s}] var  | get(\"{self._id}.{var_name}\")")
        else:
            prefix = f"[{caller_str:>12s}] ipc  | "
            print(_fmt_debug_call(prefix, f"{self._id}.{m}", a, kw))
        return is_var_access, True

    def _raise_timeout(self, m: str):
        """RMI 타임아웃 에러 출력 + raise (공통)."""
        caller_name = self._caller or "(external)"
        rmi_timeout_error(caller_name, self._id, m, self._timeout)
        raise RmiTimeoutError(caller_name, self._id, m, self._timeout)

    def _wake_dispatcher(self):
        """★ Phase 1-3: InvokeDispenser 조건부 깨움 (공통 추출)"""
        _ne = self._notify_event
        if _ne is not None:
            _ts = self._target_sleeping
            if _ts is None or _ts.value != 0:
                try: _ne.set()
                except Exception: pass

    def _call(self, m: str, *a, _chain: list = None, **kw):
        # ★ UI 스레드 경보 (방안 E): Qt 미설치 시 플래그 체크로 함수 호출 제거
        if _qt_ui.available and _qt_ui.is_ui_thread():
            import warnings
            display_m = a[0] if m in ('__getvar__', '__setvar__') and a else m
            warnings.warn(
                f"[ALASKA] Sync RMI '{self._id}.{display_m}()' called from UI thread "
                f"(caller={self._caller}). This may cause deadlock. "
                f"Use .nowait() or .async_call() instead.",
                stacklevel=3
            )

        # Caller-side debug output
        is_var_access, should_trace = self._trace_call(m, a, kw)

        # Fast path: skip measurement when no counters
        tx = self._tx_cnt
        if tx:
            tx.value += 1
            t0 = time.perf_counter()

        caller = self._caller
        # Chain for circular call detection (set for O(1) lookup)
        # ★ v2.6: thread-local chain 자동 병합 (A↔B 상호 호출 감지)
        chain = _build_chain(_chain, getattr(_rmi_chain_local, 'chain', None), caller)

        # ★ Binary RMI request (Phase 1: pickle 최소화)
        seq = (self._req_seq + 1) & 0xFFFF
        object.__setattr__(self, '_req_seq', seq)

        # C11: RMI 트레이스 (stats 활성 시만 — 문자열 포맷 제거)
        _bk = self._broker
        if _bk and _bk._stats_local.get('__enabled', False):
            try: _bk.report_trace(f"RMI::{m}", self._caller or "external", f"-> {self._id} {a}")
            except Exception: pass

        # ★ Self-RMI 감지 (InvokeDispenser 데드락 방지)
        if caller and caller == self._id:
            raise InvokeDeadlockError(caller, self._id)

        # ── Binary pack request (M-08: struct fast-path + pickle fallback) ──
        _m_enc = m.encode() if isinstance(m, str) else m
        _c_enc = self._caller_enc  # ★ F05: 캐시된 바이트열 사용
        _P = RmiProtocol
        _pack_req = _P.ST_REQ.pack
        _BIN_REQ = _P.BIN_REQ

        if not a and not kw and caller:
            # ── No args: struct only ──
            if chain:
                _hdr = _pack_req(_BIN_REQ, seq, 0x02, len(_m_enc), len(_c_enc))
                _parts = [_hdr, _m_enc, _c_enc, bytes([len(chain)])]
                for _cid in chain:
                    _cb = _cid.encode() if isinstance(_cid, str) else _cid
                    _parts.append(bytes([len(_cb)]))
                    _parts.append(_cb)
                data = b''.join(_parts)
            else:
                _hdr = _pack_req(_BIN_REQ, seq, 0x00, len(_m_enc), len(_c_enc))
                data = _hdr + _m_enc + _c_enc
        elif a and not kw and not chain and caller:
            # ── M-08: struct fast-path for simple args ──
            _na = len(a)
            _a0 = a[0]
            _flag = 0
            if _na == 1:
                _t0 = type(_a0)  # C13: identity 비교
                if _t0 is int:
                    _flag = _P.FLAG_INT1
                    _tail = _P.ST_I64.pack(_a0)
                elif _t0 is float:
                    _flag = _P.FLAG_FLOAT1
                    _tail = _P.ST_F64.pack(_a0)
                elif _t0 is str:
                    _sb = _a0.encode()
                    if len(_sb) <= 0xFFFF:
                        _flag = _P.FLAG_STR1
                        _tail = _P.ST_U16.pack(len(_sb)) + _sb
            elif _na == 2:
                _a1 = a[1]
                _t0, _t1 = type(_a0), type(_a1)  # C13
                if _t0 is int and _t1 is int:
                    _flag = _P.FLAG_INT2
                    _tail = _P.ST_I64x2.pack(_a0, _a1)
                elif _t0 is float and _t1 is float:
                    _flag = _P.FLAG_FLOAT2
                    _tail = _P.ST_F64x2.pack(_a0, _a1)
            if _flag:
                _hdr = _pack_req(_BIN_REQ, seq, _flag, len(_m_enc), len(_c_enc))
                data = _hdr + _m_enc + _c_enc + _tail
            else:
                # Fallback: pickle
                _td = {'a': a}
                _hdr = _pack_req(_BIN_REQ, seq, 0x01, len(_m_enc), len(_c_enc))
                data = _hdr + _m_enc + _c_enc + pickle.dumps(_td, protocol=5)
        else:
            # ── Pickle fallback (kw, chain+args, external caller) ──
            _td = {}
            if a: _td['a'] = a
            if kw: _td['kw'] = kw
            if chain: _td['chain'] = chain
            if not caller: _td['rq'] = self._res_q
            _hdr = _pack_req(_BIN_REQ, seq, 0x01, len(_m_enc), len(_c_enc))
            data = _hdr + _m_enc + _c_enc + pickle.dumps(_td, protocol=5)
        # F4: payload 크기 검사 (buffer capacity = size - 5)
        _rq = self._req_q
        if len(data) + 4 > _rq.size - 1:
            raise PayloadTooLargeError(
                f"RMI request {len(data)}B exceeds buffer capacity {_rq.size - 5}B "
                f"(caller={self._caller}, target={self._id}, method={m})")
        _write = _rq.write
        _ctx = self._req_lock or _nullcontext()
        with _ctx:
            for _wr_try in range(_MAX_WRITE_RETRY):
                if _write(data):
                    break
                time.sleep(0)
            else:
                raise RmiTimeoutError(self._caller or "(external)", self._id, m, self._timeout,
                                      reason="request queue full")

        self._wake_dispatcher()

        # ── 응답 대기 (Phase 1: Binary/Pickle 자동 감지) ──
        # ★ _resp_lock: async_call() 사용 시만 활성화 (SPSC 보호, 미사용 시 0 오버헤드)
        _read = self._res_q.read
        _resp_evt = self._response_event
        _use_lock = self._has_async
        _resp_lock = self._resp_lock if _use_lock else None
        _unpack = RmiProtocol.unpack_response
        # ★ v2.6 C2: 통합 응답 대기 루프 (event-wait / spin-wait 공통)
        _deadline = 0.0 if _resp_evt else (time.perf_counter() + self._timeout)
        _spin = 0
        while True:
            if _resp_lock:
                with _resp_lock:
                    resp = _read()
            else:
                resp = _read()
            if resp is not None:
                r = _unpack(resp, seq)
                if r is not None:
                    break
                continue  # stale 응답 — skip
            # Wait strategy: event-wait vs spin-wait
            _now = time.perf_counter()  # C6: 1회 캐시
            if _resp_evt is not None:
                # ★ v2.6 F07: Lazy deadline — read-first, clear+wait on miss
                if _deadline == 0.0:
                    try: _resp_evt.clear()
                    except Exception: pass
                    _deadline = _now + self._timeout
                    # F1: clear 후 재확인 (lost wakeup 방지)
                    resp = _read()
                    if resp is not None:
                        r = _unpack(resp, seq)
                        if r is not None:
                            break
                remaining = _deadline - _now
                if remaining <= 0:
                    self._raise_timeout(m)
                try: _resp_evt.wait(timeout=min(remaining, 0.05))
                except Exception: time.sleep(0)
            else:
                _spin += 1
                if _spin >= _SPIN_CHECK:
                    if _now >= _deadline:
                        self._raise_timeout(m)
                    time.sleep(0)
                    _spin = 0

        # Measurement (only if counters exist)
        if tx:
            elapsed = (time.perf_counter() - 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

        # Fast path for success
        e = r.get('e')
        if e:
            trace = r.get('trace')
            raise Exception(f"{e}\n--- Remote Traceback ---\n{trace}" if trace else e)
        result = r.get('r')
        # VAR get: 결과 수신 후 값 표시
        if is_var_access and m == '__getvar__' and should_trace:
            caller_str = self._caller or "(external)"
            var_name = a[0] if a else ''
            print(f"[{caller_str:>12s}] var  | get(\"{self._id}.{var_name}\")->{repr(result)}")
        return result

    def _call_nowait(self, m: str, *a, _chain: list = None, **kw):
        """비동기 RMI 호출 (Fire-and-Forget)"""
        # Caller-side debug output
        self._trace_call(m, a, kw, include_getvar=True)

        tx = self._tx_cnt
        if tx: tx.value += 1
        caller = self._caller
        # Chain for circular call detection (set for O(1) lookup)
        # ★ v2.6: thread-local chain 자동 병합 (A↔B 상호 호출 감지)
        chain = _build_chain(_chain, getattr(_rmi_chain_local, 'chain', None), caller)
        req = {'m': m}
        if a: req['a'] = a
        if kw: req['kw'] = kw
        if chain: req['chain'] = chain

        # ── Signal P4 큐 경유 (QoS 통합, 디스패처 v2.4) ──
        _broker = self._broker
        if _broker:
            try:
                qs = _broker._get_queues_cached(self._id)
                if qs:
                    qs[3].put_nowait({  # P4 = LOW priority
                        'name': '_rmi.nowait',
                        'source': caller or 'external',
                        'data': req,
                        'qos': {'priority': 4, 'deadline': 0.500, 'on_violation': 'IGNORE'},
                        'ts': time.time()
                    })
                    self._wake_dispatcher()
                    return
            except Exception:
                pass  # fallback to direct write

        # ── Fallback: SmRingBuffer 직접 전송 (broker 없거나 Signal 큐 실패 시) ──
        _rq = self._req_q
        if _rq:
            data = pickle.dumps(req, protocol=5)
            # F4: payload 크기 검사
            if len(data) + 4 > _rq.size - 1:
                raise PayloadTooLargeError(
                    f"RMI nowait {len(data)}B exceeds buffer capacity {_rq.size - 5}B")
            _write = _rq.write
            _ctx = self._req_lock or _nullcontext()
            with _ctx:
                for _ in range(_MAX_WRITE_RETRY):
                    if _write(data):
                        break
                    time.sleep(0)

    def _call_async(self, m: str, *a, done: str = None, error: str = None, **kw):
        """비동기 RMI + Signal 콜백 (UI 스레드 안전)

        워커 스레드에서 동기 _call()을 실행하고,
        완료 시 done signal, 에러 시 error signal을 emit한다.
        """
        if not self._has_async:
            object.__setattr__(self, '_has_async', True)
        _broker = self._broker
        worker = _make_async_worker(lambda: self._call(m, *a, **kw),
                                    done, error, _broker, self._caller)
        t = threading.Thread(target=worker, daemon=True)
        t.start()

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


# ==============================================================================
# DirectClient — Thread-Thread 직접 호출 (IPC bypass)
# ==============================================================================

class _DirectProxy(_ProxyOpsMixin):
    """Thread-Thread 직접 호출용 프록시 (RMI 없이 메서드 직접 호출)"""
    __slots__ = ('_job', '_name', '_client')

    def __init__(self, job, name: str, client: 'DirectClient' = None):
        object.__setattr__(self, '_job', job)
        object.__setattr__(self, '_name', name)
        object.__setattr__(self, '_client', client)

    def _resolve(self):
        val = getattr(self._job, self._name, None)
        return val() if callable(val) else val

    def __call__(self, *a, **kw):
        client = self._client
        try:
            if client and client._broker:
                client._broker.report_trace(f"RMI::{self._name}", client._caller or "external", f"-> {client._id} (DIRECT)")
        except Exception: pass
        if client and client._debug_method:
            caller_str = client._caller or "(external)"
            prefix = f"[{caller_str:>12s}] ipc  | "
            print(_fmt_debug_call(prefix, f"{client._id}.{self._name}", a, kw))
        fn = getattr(self._job, self._name, None)
        if fn is None:
            raise AttributeError(f"Method '{self._name}' not found")
        return fn(*a, **kw) if callable(fn) else fn

    def nowait(self, *a, **kw):
        return self(*a, **kw)

    def async_call(self, *a, done: str = None, error: str = None, **kw):
        client = self._client
        _broker = client._broker if client else None
        caller = client._caller if client else None
        job, name = self._job, self._name
        fn = getattr(job, name, None)
        call_fn = (lambda: fn(*a, **kw)) if callable(fn) else (lambda: fn)
        worker = _make_async_worker(call_fn, done, error, _broker, caller)
        t = threading.Thread(target=worker, daemon=True)
        t.start()

    def __repr__(self): return f"_DirectProxy({self._name})"


class DirectClient:
    """Thread-Thread 직접 호출 클라이언트 (IPC 없음, 최고 성능)

    Thread 모드 task 간 호출 시 Queue를 거치지 않고
    job 인스턴스의 메서드를 직접 호출합니다.
    """
    __slots__ = ('_id', '_job', '_job_getter', '_caller', '_debug_method', '_debug_variable', '_broker')

    def __init__(self, target_id: str, job_getter, caller: str = None,
                 debug_method: bool = False, debug_variable: bool = False, broker=None):
        object.__setattr__(self, '_id', target_id)
        object.__setattr__(self, '_job', None)
        object.__setattr__(self, '_job_getter', job_getter)
        object.__setattr__(self, '_caller', caller)
        object.__setattr__(self, '_debug_method', debug_method)
        object.__setattr__(self, '_debug_variable', debug_variable)
        object.__setattr__(self, '_broker', broker)

    def _get_job(self):
        job = self._job
        if job is None:
            job = self._job_getter()
            object.__setattr__(self, '_job', job)
        return job

    def __getattr__(self, name: str):
        if name[0] == '_':
            raise AttributeError(name)
        if self._debug_variable:
            caller_str = self._caller or "(external)"
            try:
                val = getattr(self._get_job(), name)
                print(f"[{caller_str:>12s}] var  | get(\"{self._id}.{name}\")->{repr(val)}")
            except Exception:
                print(f"[{caller_str:>12s}] var  | get(\"{self._id}.{name}\")")
        return _DirectProxy(self._get_job(), name, self)

    def __setattr__(self, name: str, value):
        if name[0] == '_':
            object.__setattr__(self, name, value)
        else:
            if self._debug_variable:
                caller_str = self._caller or "(external)"
                print(f"[{caller_str:>12s}] var  | set(\"{self._id}.{name}\", {repr(value)})")
            setattr(self._get_job(), name, value)

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