"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.0 - Task Monitor                                                  ║
║  HTTP-Based Web Monitoring System                                            ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.0.1                                                             ║
║  Date    : 2026-02-07                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    HTTP 기반 Task 모니터링 웹 시스템                                         ║
║    - REST API: /api/status, /api/health, /api/config, /api/performance/*     ║
║    - 시스템 정보: /api/sysinfo (OS, CPU, GPU, Memory, Network)               ║
║    - 웹 UI 대시보드                                                          ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Classes:                                                                    ║
║    MonitorHandler - HTTP 요청 핸들러                                         ║
║    TaskMonitor    - 스레드 HTTP 서버 래퍼                                    ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

from __future__ import annotations
from typing import TYPE_CHECKING
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
import json
import time
import queue
import traceback
import os
from datetime import datetime
from .task_monitor_html import render_monitor_page

try:
    import psutil
    HAS_PSUTIL = True
except ImportError:
    HAS_PSUTIL = False

if TYPE_CHECKING:
    from .task_manager import TaskManager


class MonitorHandler(BaseHTTPRequestHandler):
    manager: TaskManager = None
    start_time: float = 0
    gconfig = None
    # Memory history for leak detection graph (72 hours × 60 points = 4320 points, 1 point per minute)
    memory_history: list = []
    memory_history_max: int = 4320  # 72 hours * 60 points/hour
    memory_history_interval: int = 60  # seconds between recordings
    memory_history_last_record: float = 0

    def log_message(self, format, *args): pass

    def do_GET(self):
        routes = {
            '/api/status': self._json,
            '/api/health': self._health,
            '/api/clear': self._clear,
            '/api/cpu': self._cpu,
            '/api/config': self._config,
            '/api/gconfig': self._gconfig,
            '/api/logo': self._logo,
            '/api/rmi_methods': self._rmi_methods,
            '/api/measurement': self._get_measurement,
            '/api/smblock': self._smblock,
            '/api/subscriptions': self._subscriptions,
            '/api/performance/system': self._perf_system,
            '/api/performance/tasks': self._perf_tasks,
            '/api/sysinfo': self._sysinfo,
            '/api/sysinfo/memory_history': self._memory_history,
            '/api/external': self._external_get,
            '/api/logs': self._logs,
        }
        # Extract path without query parameters
        path_only = self.path.split('?')[0]
        # Check static routes first
        if path_only in routes:
            routes[path_only]()
            return
        # Dynamic routes for /api/performance/tasks/{id} and /api/performance/tasks/{id}/methods
        if self.path.startswith('/api/performance/tasks/'):
            parts = self.path.split('/')
            if len(parts) == 5:  # /api/performance/tasks/{id}
                self._perf_task_by_id(parts[4])
            elif len(parts) == 6 and parts[5] == 'methods':  # /api/performance/tasks/{id}/methods
                self._perf_task_methods(parts[4])
            else:
                self._send_json({'error': 'Invalid path'})
            return
        self._html()

    def do_POST(self):
        if self.path == '/api/config':
            self._update_config()
        elif self.path == '/api/gconfig':
            self._update_gconfig()
        elif self.path == '/api/stop':
            self._stop_all()
        elif self.path == '/api/measurement':
            self._set_measurement()
        elif self.path == '/api/subscriptions/toggle':
            self._subscriptions_toggle()
        elif self.path == '/api/subscriptions/clear':
            self._subscriptions_clear()
        elif self.path == '/api/external':
            self._external_set()
        elif self.path == '/api/external/test/slack':
            self._external_test('slack')
        elif self.path == '/api/external/test/email':
            self._external_test('email')
        elif self.path == '/api/external/test/webhook':
            self._external_test('webhook')
        elif self.path == '/api/external/report':
            self._external_report()

    def _config(self):
        data = []
        for tid, ti in self.manager._tasks.items():
            vars = {}
            if ti.job_class:
                keys = [k for k, v in ti.injection.items() if not (isinstance(v, str) and (v.startswith("task:") or v.startswith("smblock:")))]
                if keys:
                    try:
                        resp_q = self.manager._mgr.Queue()
                        ti.request_q.put({'m': '__get_config__', 'a': (keys,), 'kw': {}, 'rq': resp_q})
                        resp = resp_q.get(timeout=0.2)  # 200ms timeout for faster loading
                        vars = resp.get('r') or {k: ti.injection[k] for k in keys}
                    except queue.Empty:
                        vars = {k: ti.injection[k] for k in keys}  # Timeout fallback (use injection)
                    except Exception as e:
                        print(f"[Monitor] Config read error for '{tid}': {e}")
                        vars = {k: ti.injection[k] for k in keys}
                    # Filter out non-JSON-serializable values (e.g., SmBlock proxy objects)
                    vars = {k: v for k, v in vars.items() if isinstance(v, (str, int, float, bool, type(None), list, dict))}
            data.append({'id': tid, 'class': ti.task_class_name, 'vars': vars})
        self._send_json(data)

    def _rmi_methods(self):
        """RMI 메서드별 호출 통계 및 시간 측정 반환"""
        data = []
        for tid, ti in self.manager._tasks.items():
            methods = dict(ti.rmi_methods) if ti.rmi_methods else {}
            timing = dict(ti.rmi_timing) if ti.rmi_timing else {}
            total = sum(methods.values()) if methods else 0
            # Get alive status from worker
            worker = self.manager._workers.get(tid)
            alive = worker.is_alive() if worker else False

            # Build methods_timing with min/avg/max for each method
            methods_timing = {}
            for m, count in methods.items():
                t = timing.get(m)
                if t:
                    ipc_list, func_list = t
                    ipc_list = list(ipc_list) if ipc_list else []
                    func_list = list(func_list) if func_list else []
                    if ipc_list and func_list:
                        methods_timing[m] = {
                            'count': count,
                            'ipc': {'min': round(min(ipc_list), 2), 'avg': round(sum(ipc_list)/len(ipc_list), 2), 'max': round(max(ipc_list), 2)},
                            'func': {'min': round(min(func_list), 2), 'avg': round(sum(func_list)/len(func_list), 2), 'max': round(max(func_list), 2)},
                        }
                    else:
                        methods_timing[m] = {'count': count, 'ipc': None, 'func': None}
                else:
                    methods_timing[m] = {'count': count, 'ipc': None, 'func': None}

            data.append({
                'id': tid,
                'class': ti.task_class_name,
                'mode': ti.mode,
                'alive': alive,
                'total': total,
                'methods': methods,
                'methods_timing': methods_timing
            })
        self._send_json(data)

    def _get_measurement(self):
        """RMI 메서드 측정 상태 조회"""
        enabled = self.manager._measurement.value if self.manager._measurement else False
        self._send_json({'enabled': enabled})

    def _set_measurement(self):
        """RMI 메서드 측정 on/off 토글"""
        try:
            length = int(self.headers.get('Content-Length', 0))
            data = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            enabled = data.get('enabled')
            if enabled is None:
                # Toggle if no value specified
                enabled = not self.manager._measurement.value
            self.manager._measurement.value = bool(enabled)
            self._send_json({'status': 'ok', 'enabled': self.manager._measurement.value})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _sysinfo(self):
        """시스템 정보 조회"""
        from .task_monitor_sysinfo import HwInfoCollector
        try:
            data = HwInfoCollector.collect_all()
            # Record memory history for leak detection (once per minute)
            mem = data.get('memory', {})
            if mem:
                import time
                now = time.time()
                # Only record if interval has passed
                if now - MonitorHandler.memory_history_last_record >= MonitorHandler.memory_history_interval:
                    record = {
                        'ts': now,
                        'used_gb': mem.get('used_gb', 0),
                        'percent': mem.get('usage_percent', 0)
                    }
                    MonitorHandler.memory_history.append(record)
                    MonitorHandler.memory_history_last_record = now
                    # Trim to max size
                    if len(MonitorHandler.memory_history) > MonitorHandler.memory_history_max:
                        MonitorHandler.memory_history = MonitorHandler.memory_history[-MonitorHandler.memory_history_max:]
            self._send_json(data)
        except Exception as e:
            self._send_json({'error': str(e), 'trace': traceback.format_exc()})

    def _memory_history(self):
        """메모리 히스토리 조회 (메모리 릭 그래프용)"""
        self._send_json({
            'history': MonitorHandler.memory_history,
            'max_points': MonitorHandler.memory_history_max
        })

    def _logs(self):
        """로그 조회"""
        from urllib.parse import urlparse, parse_qs
        try:
            # Parse query parameters
            parsed = urlparse(self.path)
            params = parse_qs(parsed.query)
            count = int(params.get('count', [100])[0])
            level = params.get('level', [None])[0]
            task_name = params.get('task', [None])[0]

            # Get log_task client
            log_task_info = self.manager._tasks.get('log_task')
            if not log_task_info:
                # Try to find any task with log_task class
                for tid, ti in self.manager._tasks.items():
                    if ti.task_class_name == 'log_task':
                        log_task_info = ti
                        break

            if log_task_info:
                try:
                    resp_q = self.manager._mgr.Queue()
                    log_task_info.request_q.put({
                        'm': 'get_recent_logs',
                        'a': (),
                        'kw': {'count': count, 'level': level, 'task_name': task_name},
                        'rq': resp_q
                    })
                    resp = resp_q.get(timeout=2.0)
                    if resp.get('e'):
                        self._send_json({'error': resp['e'], 'logs': []})
                    else:
                        self._send_json({'logs': resp.get('r', [])})
                except queue.Empty:
                    self._send_json({'error': 'LogTask timeout', 'logs': []})
            else:
                # Fallback: read directly from log files
                from pathlib import Path
                from .task_log import LEVEL_MAP
                import json as json_module

                # Check platform_config first (new format), then task_config (old format)
                log_dir = "logs"
                if self.gconfig:
                    log_dir = self.gconfig.data_get("platform_config._log/log_task.log_dir") or \
                              self.gconfig.data_get("task_config.log/log_task.log_dir") or "logs"
                log_path = Path(log_dir)

                if not log_path.exists():
                    self._send_json({'logs': [], 'warning': 'Log directory not found'})
                    return

                level_no = LEVEL_MAP.get(level, 0) if level else 0
                results = []
                log_files = sorted(log_path.glob("*.log"), key=lambda f: f.stat().st_mtime, reverse=True)

                for log_file in log_files[:5]:  # 최근 5개 파일만
                    if len(results) >= count:
                        break
                    try:
                        with open(log_file, "r", encoding="utf-8") as f:
                            lines = f.readlines()
                        for line in reversed(lines):
                            if len(results) >= count:
                                break
                            try:
                                record = json_module.loads(line.strip())
                                if level_no > 0:
                                    rec_level_no = LEVEL_MAP.get(record.get("lv", ""), 0)
                                    if rec_level_no < level_no:
                                        continue
                                if task_name and record.get("task") != task_name:
                                    continue
                                results.append(record)
                            except:
                                continue
                    except:
                        continue

                self._send_json({'logs': results})
        except Exception as e:
            self._send_json({'error': str(e), 'logs': []})

    def _external_get(self):
        """외부연동 설정 조회"""
        from .task_monitor_ext_link import ExternalLinkManager
        try:
            ext_mgr = ExternalLinkManager(self.gconfig, self.manager)
            self._send_json(ext_mgr.get_config())
        except Exception as e:
            self._send_json({'error': str(e)})

    def _external_set(self):
        """외부연동 설정 저장"""
        from .task_monitor_ext_link import ExternalLinkManager
        try:
            length = int(self.headers.get('Content-Length', 0))
            data = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            ext_mgr = ExternalLinkManager(self.gconfig, self.manager)
            success = ext_mgr.set_config(data)
            self._send_json({'status': 'ok' if success else 'error'})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _external_test(self, target: str):
        """외부연동 테스트"""
        from .task_monitor_ext_link import ExternalLinkManager
        try:
            ext_mgr = ExternalLinkManager(self.gconfig, self.manager)
            if target == 'slack':
                result = ext_mgr.test_slack()
            elif target == 'email':
                result = ext_mgr.test_email()
            elif target == 'webhook':
                result = ext_mgr.test_webhook()
            else:
                result = {'success': False, 'error': 'Invalid target'}
            self._send_json(result)
        except Exception as e:
            self._send_json({'success': False, 'error': str(e)})

    def _external_report(self):
        """Health Report 즉시 발송"""
        from .task_monitor_ext_link import ExternalLinkManager
        try:
            length = int(self.headers.get('Content-Length', 0))
            data = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            targets = data.get('targets')
            ext_mgr = ExternalLinkManager(self.gconfig, self.manager)
            results = ext_mgr.send_report(targets)
            self._send_json({'status': 'ok', 'results': results})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _smblock(self):
        """SmBlock 공유 메모리 풀 통계 조회"""
        from .task_manager import SmBlockHandler
        data = []

        # Find which tasks use each smblock
        smblock_users = {}  # name -> [task_ids]
        for tid, ti in self.manager._tasks.items():
            for k, v in ti.injection.items():
                if isinstance(v, dict) and "_smblock" in v:
                    pool_name = v["_smblock"]
                    if pool_name not in smblock_users:
                        smblock_users[pool_name] = []
                    smblock_users[pool_name].append(f"{tid}.{k}")

        for name, pool in SmBlockHandler._pools.items():
            try:
                free_count = pool.count()
                used_count = pool.maxsize - free_count
                usage_pct = (used_count / pool.maxsize * 100) if pool.maxsize > 0 else 0
                pool_data = {
                    'name': name,
                    'shape': list(pool.shape),
                    'maxsize': pool.maxsize,
                    'used': used_count,
                    'free': free_count,
                    'usage_pct': round(usage_pct, 1),
                    'item_size': pool.item_size,
                    'total_mb': round(pool.item_size * pool.maxsize / 1024 / 1024, 2),
                    'users': smblock_users.get(name, []),
                }
                data.append(pool_data)
            except Exception as e:
                data.append({'name': name, 'error': str(e)})

        self._send_json(data)

    def _subscriptions(self):
        """Signal 구독 현황 및 통계 조회"""
        data = {
            'mode': 'unknown',
            'subscriptions': [],
            'queues': [],
            'stats': {'enabled': False, 'cleared_at': 0, 'signals': {}},
        }
        broker = getattr(self.manager, '_signal_broker', None)
        if not broker:
            self._send_json(data)
            return
        try:
            data['mode'] = broker.mode
            # 구독자 목록 (signal_name:task_id -> True 형태)
            with broker._lock:
                all_keys = list(broker._subscribers.keys())
            subs_by_signal = {}
            for key in all_keys:
                if key == '__version__':
                    continue
                parts = key.split(':', 1)
                if len(parts) == 2:
                    signal_name, task_id = parts
                    if signal_name not in subs_by_signal:
                        subs_by_signal[signal_name] = []
                    subs_by_signal[signal_name].append(task_id)
            data['subscriptions'] = [{'signal': k, 'subscribers': v} for k, v in subs_by_signal.items()]
            # 등록된 큐 목록
            with broker._lock:
                queue_ids = list(broker._queues.keys())
            data['queues'] = queue_ids
            # 통계 정보
            data['stats'] = broker.get_stats()
        except Exception as e:
            data['error'] = str(e)
        self._send_json(data)

    def _subscriptions_toggle(self):
        """Signal 통계 수집 on/off 토글"""
        broker = getattr(self.manager, '_signal_broker', None)
        if not broker:
            self._send_json({'status': 'error', 'message': 'No signal broker'})
            return
        try:
            length = int(self.headers.get('Content-Length', 0))
            body = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            enabled = body.get('enabled')
            if enabled is None:
                # Toggle if no value specified
                enabled = not broker.stats_enabled
            broker.stats_enabled = bool(enabled)
            self._send_json({'status': 'ok', 'enabled': broker.stats_enabled})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _subscriptions_clear(self):
        """Signal 통계 초기화"""
        broker = getattr(self.manager, '_signal_broker', None)
        if not broker:
            self._send_json({'status': 'error', 'message': 'No signal broker'})
            return
        try:
            cleared_at = broker.clear_stats()
            self._send_json({'status': 'ok', 'cleared_at': cleared_at})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    # ─────────────────────────────────────────────────────────────────────────
    # Performance API (PERF-006)
    # ─────────────────────────────────────────────────────────────────────────

    def _perf_system(self):
        """시스템 전체 성능 통계 반환 (/api/performance/system)"""
        perf = self.manager.performance.get_system_performance()
        self._send_json(perf.to_dict())

    def _perf_tasks(self):
        """모든 Task 성능 통계 반환 (/api/performance/tasks)"""
        tasks = self.manager.performance.get_all_tasks_performance()
        self._send_json([t.to_dict() for t in tasks])

    def _perf_task_by_id(self, task_id: str):
        """특정 Task 성능 통계 반환 (/api/performance/tasks/{id})"""
        perf = self.manager.performance.get_task_performance(task_id)
        if perf:
            self._send_json(perf.to_dict())
        else:
            self._send_json({'error': f'Task {task_id} not found'})

    def _perf_task_methods(self, task_id: str):
        """특정 Task의 메서드별 성능 통계 반환 (/api/performance/tasks/{id}/methods)"""
        methods = self.manager.performance.get_task_methods_performance(task_id)
        self._send_json([m.to_dict() for m in methods])

    def _update_config(self):
        length = int(self.headers.get('Content-Length', 0))
        data = json.loads(self.rfile.read(length).decode())
        tid, key, value = data.get('task_id'), data.get('key'), data.get('value')
        result = {'status': 'error', 'message': 'Task not found'}
        print(f"[Monitor:_update_config] tid={tid}, key={key}, value={value}, type={type(value)}")

        if tid in self.manager._tasks:
            ti = self.manager._tasks[tid]
            old_val = ti.injection.get(key)
            print(f"[Monitor:_update_config] old_val={old_val}, type={type(old_val)}")
            if old_val is not None and not (isinstance(old_val, str) and old_val.startswith("task:")):
                try:
                    if isinstance(old_val, bool):
                        value = value.lower() in ('true', '1', 'yes')
                    elif isinstance(old_val, (int, float)):
                        # Try int first, fallback to float
                        try:
                            value = int(value)
                        except ValueError:
                            value = float(value)
                    print(f"[Monitor:_update_config] converted value={value}, type={type(value)}")
                    ti.injection[key] = value
                    print(f"[Monitor:_update_config] injection updated: {ti.injection.get(key)}")
                    resp_q = self.manager._mgr.Queue()
                    print(f"[Monitor:_update_config] sending RMI __set_config__ to {tid}")
                    ti.request_q.put({'m': '__set_config__', 'a': (key, value), 'kw': {}, 'rq': resp_q})
                    try:
                        resp = resp_q.get(timeout=2.0)
                        print(f"[Monitor:_update_config] RMI response: {resp}")
                        result = {'status': 'ok', 'task_id': tid, 'key': key, 'value': value} if resp.get('r') else {'status': 'error', 'message': resp.get('e', 'Unknown')}
                    except queue.Empty:
                        print(f"[Monitor:_update_config] RMI timeout for {tid}")
                        result = {'status': 'error', 'message': f'Timeout waiting for task {tid}'}
                    except Exception as e:
                        print(f"[Monitor:_update_config] RMI exception: {e}")
                        result = {'status': 'error', 'message': f'RMI error: {e}'}
                except Exception as e:
                    print(f"[Monitor:_update_config] exception: {e}")
                    result = {'status': 'error', 'message': str(e)}
        else:
            print(f"[Monitor:_update_config] task {tid} not found in {list(self.manager._tasks.keys())}")
        self._send_json(result)

    def _gconfig(self):
        if not self.gconfig:
            self._send_json({'error': 'GConfig not configured', 'data': None})
            return
        try:
            data = self.gconfig.to_dict() if hasattr(self.gconfig, 'to_dict') else self.gconfig._cache._data
            filepath = getattr(self.gconfig, '_filepath', None)
            home_dir = os.path.dirname(filepath) if filepath else None
            log_dir = os.path.join(home_dir, 'log') if home_dir else None
            self._send_json({
                'data': data,
                'filepath': filepath,
                'home_dir': home_dir,
                'log_dir': log_dir
            })
        except Exception as e:
            self._send_json({'error': str(e), 'data': None})

    def _update_gconfig(self):
        if not self.gconfig:
            self._send_json({'status': 'error', 'message': 'GConfig not configured'})
            return
        try:
            length = int(self.headers.get('Content-Length', 0))
            data = json.loads(self.rfile.read(length).decode())
            path, value, vtype = data.get('path'), data.get('value'), data.get('vtype')
            if not path:
                self._send_json({'status': 'error', 'message': 'Path required'})
                return
            # Convert value type based on original type (vtype)
            if isinstance(value, str):
                if vtype == 'str':
                    pass  # Keep as string
                elif vtype == 'bool':
                    value = value.lower() in ('true', '1', 'yes')
                elif vtype == 'null':
                    value = None if value.lower() in ('null', '') else value
                elif vtype == 'num':
                    try: value = int(value)
                    except ValueError:
                        try: value = float(value)
                        except ValueError: pass
                else:
                    # Fallback: auto-detect type
                    if value.lower() == 'true': value = True
                    elif value.lower() == 'false': value = False
                    elif value.lower() == 'null': value = None
                    else:
                        try: value = int(value)
                        except ValueError:
                            try: value = float(value)
                            except ValueError: pass
            self.gconfig.data_set(path, value)
            self._send_json({'status': 'ok', 'path': path, 'value': value})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _stop_all(self):
        print(f"[Monitor:_stop_all] called")
        try:
            # skip_monitor=True to avoid deadlock (HTTP request waiting for server.shutdown)
            print(f"[Monitor:_stop_all] calling manager.stop_all(skip_monitor=True)")
            self.manager.stop_all(skip_monitor=True)
            print(f"[Monitor:_stop_all] manager.stop_all() completed")
            self._send_json({'status': 'ok', 'message': 'All tasks stopped'})
        except Exception as e:
            print(f"[Monitor:_stop_all] exception: {e}")
            self._send_json({'status': 'error', 'message': str(e)})

    def _logo(self):
        logo_path = os.path.join(os.path.dirname(__file__), 'div_logo.png')
        try:
            with open(logo_path, 'rb') as f:
                data = f.read()
            self.send_response(200)
            self.send_header('Content-Type', 'image/png')
            self.send_header('Cache-Control', 'max-age=86400')
            self.end_headers()
            self.wfile.write(data)
        except FileNotFoundError:
            self.send_response(404)
            self.end_headers()

    def _cpu(self):
        if not HAS_PSUTIL:
            self._send_json({'error': 'psutil not installed', 'tasks': []})
            return
        tasks = []
        for tid, ti in self.manager._tasks.items():
            w = self.manager._workers.get(tid)
            alive = w.is_alive() if w else False
            rmi_fail = ti.rmi_fail.value if ti and ti.rmi_fail else 0
            restart = ti.restart_cnt.value if ti and ti.restart_cnt else 0
            mode = ti.mode if ti else ''
            task_data = {'id': tid, 'class': ti.task_class_name if ti else '', 'mode': mode, 'alive': alive, 'rmi_fail': rmi_fail, 'restart': restart, 'pid': None, 'cpu': 0, 'memory': 0, 'threads': 0, 'vars': {}}
            try:
                if hasattr(w, 'pid') and w.pid:
                    p = psutil.Process(w.pid)
                    task_data['pid'] = w.pid
                    task_data['cpu'] = p.cpu_percent(interval=0.1)
                    task_data['memory'] = p.memory_info().rss / 1024 / 1024
                    task_data['threads'] = p.num_threads()
            except psutil.NoSuchProcess:
                pass  # Process ended
            except Exception as e:
                print(f"[Monitor] CPU info error for '{tid}': {e}")
            # Add config vars (filtered)
            if ti and ti.job_class:
                keys = [k for k, v in ti.injection.items() if not (isinstance(v, str) and (v.startswith("task:") or v.startswith("smblock:"))) and not (isinstance(v, dict) and "_smblock" in v)]
                if keys:
                    try:
                        resp_q = self.manager._mgr.Queue()
                        ti.request_q.put({'m': '__get_config__', 'a': (keys,), 'kw': {}, 'rq': resp_q})
                        resp = resp_q.get(timeout=0.2)
                        vars_data = resp.get('r') or {k: ti.injection[k] for k in keys}
                    except Exception:
                        vars_data = {k: ti.injection[k] for k in keys}
                    # Filter non-serializable
                    task_data['vars'] = {k: v for k, v in vars_data.items() if isinstance(v, (str, int, float, bool, type(None), list, dict))}
            tasks.append(task_data)
        self._send_json({'system_cpu': psutil.cpu_percent(interval=0.1),
                         'system_memory': psutil.virtual_memory().percent, 'tasks': tasks})

    def _clear(self):
        self.manager.clear_stats()
        self._send_json({'status': 'cleared'})

    def _json(self):
        self._send_json(self._status())

    def _health(self):
        """Health check with full statistics (기존통계 + RMI 메서드 통계)"""
        status = self.manager.get_status()
        uptime = int(time.time() - self.start_time)

        # Build task stats with rmi_methods
        tasks = []
        for tid, info in status.items():
            tasks.append({
                'id': tid,
                'class': info['class'],
                'mode': info['mode'],
                'alive': info['alive'],
                'rmi_rx': info['rmi_rx'],
                'rmi_tx': info['rmi_tx'],
                'rmi_fail': info['rmi_fail'],
                'rmi_avg': info['rmi_avg'],
                'rmi_methods': info.get('rmi_methods', {}),
            })

        self._send_json({
            'status': 'ok',
            'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'uptime_seconds': uptime,
            'tasks': tasks
        })

    def _send_json(self, data):
        try:
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps(data, indent=2).encode())
        except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError):
            pass  # 클라이언트 연결 종료 - 무시

    def _html(self):
        try:
            self.send_response(200)
            self.send_header('Content-Type', 'text/html; charset=utf-8')
            self.end_headers()
            self.wfile.write(self._page().encode())
        except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError):
            pass  # 클라이언트 연결 종료 - 무시

    def _status(self):
        uptime = int(time.time() - self.start_time)
        h, r = divmod(uptime, 3600)
        mi, s = divmod(r, 60)
        status = self.manager.get_status()
        tasks = []
        running = 0
        for tid, info in status.items():
            if info['alive']: running += 1
            tasks.append({
                'id': tid, 'class': info['class'], 'mode': info['mode'],
                'status': 'running' if info['alive'] else 'stopped',
                'rmi_rx': info['rmi_rx'], 'rmi_tx': info['rmi_tx'], 'rmi_fail': info['rmi_fail'],
                'rmi_avg': info['rmi_avg'], 'rmi_min': info['rmi_min'], 'rmi_max': info['rmi_max'],
                'rxq_size': info['rxq_size'], 'txq_size': info['txq_size'], 'restart': info['restart'],
            })
        return {'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                'uptime': f'{h}시간 {mi}분 {s}초', 'total': len(tasks), 'running': running, 'tasks': tasks}

    def _page(self):
        """HTML 페이지 생성 (task_monitor_html 모듈 사용)."""
        status = self._status()
        app_info = {
            "name": self.gconfig.data_get("app_info.name", "") if self.gconfig else "",
            "version": self.gconfig.data_get("app_info.version", "") if self.gconfig else "",
            "id": self.gconfig.data_get("app_info.id", "") if self.gconfig else "",
        }
        return render_monitor_page(status, app_info)


class TaskMonitor:
    """웹 기반 TASK 모니터"""
    __slots__ = ('_manager', '_port', '_server', '_thread', '_running', '_gconfig')

    def __init__(self, manager: TaskManager, port: int = 8080, gconfig=None):
        self._manager = manager
        self._port = port
        self._server = None
        self._thread = None
        self._running = False
        self._gconfig = gconfig

    def start(self):
        MonitorHandler.manager = self._manager
        MonitorHandler.start_time = time.time()
        MonitorHandler.gconfig = self._gconfig
        self._server = HTTPServer(('0.0.0.0', self._port), MonitorHandler)
        self._server.timeout = 0.5  # handle_request 타임아웃 설정
        self._running = True
        self._thread = threading.Thread(target=self._serve, daemon=True)
        self._thread.start()
        print(f"[Monitor] http://localhost:{self._port}")

    def _serve(self):
        while self._running:
            self._server.handle_request()

    def stop(self):
        print("[Monitor:stop] called")
        self._running = False
        # handle_request() 타임아웃으로 자연 종료 대기
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=1.0)
        if self._server:
            self._server.server_close()
        print("[Monitor] stopped")

    @property
    def port(self) -> int: return self._port

    @property
    def url(self) -> str: return f"http://localhost:{self._port}"

    def __enter__(self): self.start(); return self
    def __exit__(self, *_): self.stop(); return False
