# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.0 - Task Monitor                                                  ║
║  HTTP-Based Web Monitoring System                                            ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.1.0                                                             ║
║  Date    : 2026-02-11                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  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, Optional
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
from concurrent.futures import ThreadPoolExecutor
import threading
import json
import time
import queue
import pickle
import multiprocessing
import os
from datetime import datetime
from .. import task
from .task_monitor_html import render_monitor_page

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

if TYPE_CHECKING:
    from . import TaskManager


# ═══════════════════════════════════════════════════════════════════════════════
# Threaded HTTP Server
# ═══════════════════════════════════════════════════════════════════════════════

class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """멀티스레드 HTTP 서버 (동시 요청 처리)"""
    daemon_threads = True
    allow_reuse_address = True


# ═══════════════════════════════════════════════════════════════════════════════
# Monitor Handler
# ═══════════════════════════════════════════════════════════════════════════════

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 _rmi_call(self, ti, method: str, args: tuple = (), kwargs: dict = None, timeout: float = 1.0):
        """SmRingBuffer request_q 를 통해 RMI 호출 후 multiprocessing.Queue 로 응답 수신."""
        resp_q = multiprocessing.Queue()
        req = {'m': method, 'a': args, 'kw': kwargs or {}, 'rq': resp_q}
        data = pickle.dumps(req, protocol=5)
        lock = getattr(ti, 'request_lock', None)
        if lock:
            with lock:
                while not ti.request_q.write(data):
                    time.sleep(0)
        else:
            while not ti.request_q.write(data):
                time.sleep(0)
        return resp_q.get(timeout=timeout)

    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,
            '/api/debug/flags': self._debug_flags_get,
            '/api/debug/signal/trace': self._debug_signal_trace_get,
            '/api/debug/architecture': self._debug_arch_get,
            '/api/debug/sequence': self._debug_sequence_get,
        }
        # 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
        # Dynamic routes for /api/debug/vars/{task_id}
        if path_only.startswith('/api/debug/vars/'):
            task_id = path_only.split('/')[4]
            self._debug_vars_get(task_id)
            return
        # Debugger page
        if path_only == '/debug':
            self._debug_html()
            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()
        elif self.path == '/api/debug/flags':
            self._debug_flags_set()
        elif self.path == '/api/debug/signal/trace':
            self._debug_signal_trace_set()
        elif self.path.startswith('/api/debug/vars/'):
            task_id = self.path.split('/')[4]
            self._debug_vars_set(task_id)
        else:
            self._send_json({'error': 'Not Found', 'path': self.path}, 404)

    def _config(self):
        data = []
        for tid, ti in list(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 = self._rmi_call(ti, '__get_config__', (keys,), timeout=0.2)
                        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 list(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)})

    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 list(self.manager._tasks.items()):
                    if ti.task_class_name == 'log_task':
                        log_task_info = ti
                        break

            if log_task_info:
                try:
                    resp = self._rmi_call(log_task_info, 'get_recent_logs', (),
                                         kwargs={'count': count, 'level': level, 'task_name': task_name},
                                         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 .core.task_log import LEVEL_MAP  # internal: core direct
                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 Exception:
                                continue
                    except Exception:
                        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 .sm_infra import SmBlockHandler
        data = []

        # Find which tasks use each smblock
        smblock_users = {}  # name -> [task_ids]
        for tid, ti in list(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 구독 현황 및 통계 조회 (v2.1 P2P Registry 대응)"""
        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
            
            # 1. 구독자 목록 (v2.1: _registry 활용)
            reg_items = list(broker._registry.items())
            data['subscriptions'] = [{'signal': k, 'subscribers': list(v)} for k, v in reg_items]
            
            # 2. 등록된 큐 목록
            data['queues'] = list(broker._queues.keys())
            
            # 3. 통계 정보 (v2.1 구조 -> UI 호환 구조 변환)
            raw_stats = broker.get_stats()
            data['stats']['enabled'] = raw_stats.get('enabled', True)
            
            for name, s in raw_stats.get('signals', {}).items():
                # VIOLATION:: 접두사 제거 후 원본 이름으로 통계 병합 시도 (선택 사항)
                clean_name = name.replace("VIOLATION::", "")
                
                if clean_name in data['stats']['signals']:
                    # 이미 존재하면 카운트 합산 및 최신 시각 유지
                    prev = data['stats']['signals'][clean_name]
                    data['stats']['signals'][clean_name] = {
                        'count': prev['count'] + s.get('cnt', 0),
                        'last_time': max(prev['last_time'], s.get('last', 0))
                    }
                else:
                    data['stats']['signals'][clean_name] = {
                        'count': s.get('cnt', 0),
                        'last_time': s.get('last', 0)
                    }
        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)}")

        ti = self.manager._tasks.get(tid)
        if ti:
            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)}")
                    print(f"[Monitor:_update_config] sending RMI __set_config__ to {tid}")
                    try:
                        resp = self._rmi_call(ti, '__set_config__', (key, value), 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 list(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 = self._rmi_call(ti, '__get_config__', (keys,), 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, status_code: int = 200):
        try:
            self.send_response(status_code)
            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)

    # ─────────────────────────────────────────────────────────────────────────
    # Debug API handlers
    # ─────────────────────────────────────────────────────────────────────────

    def _debug_flags_get(self):
        """GET /api/debug/flags - 전체 Task debug 플래그 조회"""
        result = {}
        for tid, ti in list(self.manager._tasks.items()):
            try:
                resp = self._rmi_call(ti, '__get_debug__', timeout=0.5)
                if resp.get('r'):
                    result[tid] = resp['r']
                else:
                    result[tid] = {'signal': False, 'method': False, 'variable': False, 'error': resp.get('e')}
            except queue.Empty:
                result[tid] = {'signal': False, 'method': False, 'variable': False, 'error': 'timeout'}
            except Exception as e:
                result[tid] = {'signal': False, 'method': False, 'variable': False, 'error': str(e)}
        self._send_json(result)

    def _debug_flags_set(self):
        """POST /api/debug/flags - debug 플래그 변경"""
        try:
            length = int(self.headers.get('Content-Length', 0))
            data = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            task_id = data.get('task_id')
            flag = data.get('flag')
            value = data.get('value')
            if not task_id or not flag:
                self._send_json({'status': 'error', 'message': 'task_id and flag required'}, 400)
                return
            ti = self.manager._tasks.get(task_id)
            if not ti:
                self._send_json({'status': 'error', 'message': f'Task {task_id} not found'}, 404)
                return
            resp = self._rmi_call(ti, '__set_debug__', (flag, bool(value)), timeout=1.0)
            if resp.get('r'):
                self._send_json({'status': 'ok', 'task_id': task_id, 'flag': flag, 'value': bool(value)})
            else:
                self._send_json({'status': 'error', 'message': resp.get('e', 'Unknown')})
        except queue.Empty:
            self._send_json({'status': 'error', 'message': 'Timeout'})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _debug_vars_get(self, task_id: str):
        """GET /api/debug/vars/{task_id} - Task 인스턴스 변수 조회"""
        ti = self.manager._tasks.get(task_id)
        if not ti:
            self._send_json({'error': f'Task {task_id} not found'}, 404)
            return
        try:
            resp = self._rmi_call(ti, '__list_vars__', timeout=1.0)
            if resp.get('r') is not None:
                self._send_json({
                    'task_id': task_id,
                    'class': ti.task_class_name,
                    'variables': resp['r'],
                })
            else:
                self._send_json({'error': resp.get('e', 'Unknown')})
        except queue.Empty:
            self._send_json({'error': f'Timeout querying {task_id}'})
        except Exception as e:
            self._send_json({'error': str(e)})

    def _debug_vars_set(self, task_id: str):
        """POST /api/debug/vars/{task_id} - Task 변수 값 변경"""
        ti = self.manager._tasks.get(task_id)
        if not ti:
            self._send_json({'error': f'Task {task_id} not found'}, 404)
            return
        try:
            length = int(self.headers.get('Content-Length', 0))
            data = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            name = data.get('name')
            value = data.get('value')
            if not name:
                self._send_json({'status': 'error', 'message': 'name required'}, 400)
                return
            resp = self._rmi_call(ti, '__setvar__', (name, value), timeout=1.0)
            if resp.get('r') is not None:
                self._send_json({'status': 'ok', 'task_id': task_id, 'name': name, 'value': value})
            else:
                self._send_json({'status': 'error', 'message': resp.get('e', 'Unknown')})
        except queue.Empty:
            self._send_json({'status': 'error', 'message': f'Timeout setting var on {task_id}'})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _debug_signal_trace_get(self):
        """GET /api/debug/signal/trace - Signal trace 이벤트 조회"""
        from urllib.parse import urlparse, parse_qs
        broker = getattr(self.manager, '_signal_broker', None)
        if not broker:
            self._send_json({'recording': False, 'events': [], 'error': 'No signal broker'})
            return
        try:
            parsed = urlparse(self.path)
            params = parse_qs(parsed.query)
            limit = int(params.get('limit', [100])[0])
            sig_filter = params.get('filter', [None])[0]

            events = broker.get_trace(limit)
            if sig_filter:
                events = [e for e in events if sig_filter in e.get('signal', '')]
            self._send_json({
                'recording': broker.trace_enabled,
                'events': events,
            })
        except Exception as e:
            self._send_json({'recording': False, 'events': [], 'error': str(e)})

    def _debug_signal_trace_set(self):
        """POST /api/debug/signal/trace - trace 시작/중지/초기화"""
        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))
            data = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            action = data.get('action', 'start')
            if action == 'start':
                broker.set_trace(True)
                self._send_json({'status': 'ok', 'recording': True})
            elif action == 'stop':
                broker.set_trace(False)
                self._send_json({'status': 'ok', 'recording': False})
            elif action == 'clear':
                broker.clear_trace()
                self._send_json({'status': 'ok', 'recording': broker._trace_enabled})
            else:
                self._send_json({'status': 'error', 'message': f'Unknown action: {action}'})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _debug_arch_get(self):
        """GET /api/debug/architecture - 시스템 구성도 데이터 조회"""
        try:
            if self.manager:
                arch_map = self.manager.generate_architecture_map()
                self._send_json({'status': 'ok', 'map': arch_map})
            else:
                self._send_json({'status': 'error', 'message': 'Manager not available'})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _debug_sequence_get(self):
        """GET /api/debug/sequence - 정적 시퀀스 다이어그램 조회"""
        try:
            if self.manager:
                seq_map = self.manager.generate_sequence_map()
                self._send_json({'status': 'ok', 'map': seq_map})
            else:
                self._send_json({'status': 'error', 'message': 'Manager not available'})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _debug_html(self):
        """GET /debug - 디버거 웹 UI 페이지"""
        try:
            from .task_monitor_debug_html import render_debug_page
            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 "",
            }
            html = render_debug_page(app_info)
            self.send_response(200)
            self.send_header('Content-Type', 'text/html; charset=utf-8')
            self.end_headers()
            self.wfile.write(html.encode())
        except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError):
            pass
        except Exception as e:
            self._send_json({'error': f'Debug page error: {e}'}, 500)


@task(mode="thread")
class TaskMonitor:
    """
    웹 기반 TASK 모니터 (ALASKA v2.1 표준 태스크)
    """

    def __init__(self, manager=None, port: int = 8080, gconfig=None):
        """
        Args:
            manager: TaskManager 인스턴스 (수동 생성 시)
            port: HTTP 서버 포트
            gconfig: GConfig 인스턴스 (수동 생성 시)
        """
        self.port = port
        self.manager_injected = manager 
        self.gconfig_injected = gconfig
        self._server = None
        self._manual_run = False # TaskManager 직접 시작 플래그

    def start(self):
        """TaskManager 수동 시작용 (호환성 유지)"""
        self._manual_run = True
        self._th = threading.Thread(target=self.run, daemon=True)
        self._th.start()

    def stop(self):
        """TaskManager 수동 중지용 (호환성 유지)"""
        self._manual_run = False
        if self._server:
            self._server.server_close()

    def run(self):
        """태스크 실행 엔트리 포인트"""
        # Handler 설정 (runtime 주입 우선, 없으면 생성자 주입 사용)
        rt = getattr(self, 'runtime', None)
        MonitorHandler.manager = getattr(rt, 'manager', self.manager_injected)
        MonitorHandler.gconfig = getattr(rt, 'gconfig', self.gconfig_injected)
        MonitorHandler.start_time = time.time()

        # ThreadedHTTPServer 실행
        try:
            print(f"[Monitor] Starting Web Inspector on http://localhost:{self.port} ...")
            self._server = ThreadedHTTPServer(('0.0.0.0', self.port), MonitorHandler)
            self._server.timeout = 0.5
            
            # 태스크 모드(self.running) 또는 수동 모드(self._manual_run) 지원
            while (getattr(self, 'running', False) or self._manual_run):
                self._server.handle_request()
        except Exception as e:
            if self._manual_run or getattr(self, 'running', False):
                print(f"[Monitor] Server error: {e}")
        finally:
            if self._server:
                self._server.server_close()

    def get_url(self) -> str: 
        """현재 서버 접속 URL 반환"""
        return f"http://localhost:{self.port}"
