#!/usr/bin/env python3
"""
Heartbeat Check - 系统健康检查脚本
优先读取当前运行中的后台状态接口，再回退到进程与桥接文件检测。
"""
import json
import os
import pathlib
import socket
import subprocess
import sys
import time
import urllib.request

import psutil

ROOT = pathlib.Path(__file__).parent.parent.absolute()
_FROZEN = getattr(sys, "frozen", False)
GUI_PORT = int(os.environ.get("GUI_PORT", "18790"))
GATEWAY_PORT = int(os.environ.get("OPENCLAW_GATEWAY_PORT", "18789"))

# ── MT5 基础目录（跨平台，从 config_manager 动态获取） ───────────────────────
_env_mt5 = os.environ.get("EA_BRIDGE_FILES_DIR", "").strip()
if _env_mt5:
    _MT5_BASE = pathlib.Path(_env_mt5)
elif _FROZEN:
    try:
        from config_manager import get_mt5_dir as _get_mt5_dir

        _MT5_BASE = pathlib.Path(_get_mt5_dir())
    except Exception:
        _MT5_BASE = ROOT / "bridge" / "Files"
else:
    try:
        from config_manager import get_mt5_dir as _get_mt5_dir

        _MT5_BASE = pathlib.Path(_get_mt5_dir())
    except Exception:
        _MT5_BASE = ROOT / "bridge" / "Files"

MT5_COMMON = _MT5_BASE / "gold_autotrade" / "bridge"


def _omlx_token():
    return os.environ.get("OMLX_API_KEY", "")


def _process_cmdline(proc: psutil.Process) -> str:
    try:
        cmdline = proc.cmdline()
    except Exception:
        return ""
    return " ".join(str(part) for part in cmdline if part)


def _iter_process_matches(matchers):
    for proc in psutil.process_iter(["pid", "name", "create_time", "status", "cmdline"]):
        try:
            name = str(proc.info.get("name") or "").lower()
            cmdline = _process_cmdline(proc).lower()
            if any(matcher(name, cmdline) for matcher in matchers):
                yield proc
        except Exception:
            continue


def _port_open(port: int, host: str = "127.0.0.1", timeout: float = 1.5) -> bool:
    try:
        with socket.create_connection((host, port), timeout=timeout):
            return True
    except OSError:
        return False


def _request_json(url: str, *, timeout: float = 4.0, headers: dict | None = None):
    req = urllib.request.Request(url, headers=headers or {})
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        body = resp.read()
    if not body:
        return {}
    return json.loads(body.decode("utf-8", errors="replace"))


def _format_uptime(start_ts: float) -> str:
    elapsed = max(0, int(time.time() - float(start_ts)))
    hours, rem = divmod(elapsed, 3600)
    minutes = rem // 60
    return f"{hours:02d}:{minutes:02d}"


def check_backend():
    """检查当前后台接口。"""
    ping_url = f"http://127.0.0.1:{GUI_PORT}/api/ping"
    status_url = f"http://127.0.0.1:{GUI_PORT}/api/status"
    build_url = f"http://127.0.0.1:{GUI_PORT}/api/runtime/build-info"
    try:
        ping_payload = _request_json(ping_url, timeout=3.0)
        status_payload = _request_json(status_url, timeout=5.0)
        build_payload = _request_json(build_url, timeout=3.0)
        backend_meta = build_payload.get("backend", {}) if isinstance(build_payload.get("backend", {}), dict) else {}
        frontend_meta = build_payload.get("frontend", {}) if isinstance(build_payload.get("frontend", {}), dict) else {}
        return (
            "RUNNING",
            {
                "ping_ok": bool(ping_payload.get("ok")),
                "is_running": bool(status_payload.get("isRunning", False)),
                "mt5_online": bool(status_payload.get("mt5Online", False)),
                "bridge_ok": bool(status_payload.get("bridgeOk", False)),
                "light": str(status_payload.get("light", "")),
                "price_source": str(status_payload.get("priceSource", "")),
                "price_age": status_payload.get("priceAge"),
                "bid": status_payload.get("bid"),
                "ask": status_payload.get("ask"),
                "app_version": str(
                    backend_meta.get("appVersion")
                    or ping_payload.get("appVersion")
                    or ""
                ),
                "runtime_mode": str(backend_meta.get("runtimeMode", "") or ""),
                "frontend_entry_asset": str(frontend_meta.get("entryAsset", "") or ""),
                "frontend_index_mtime": str(frontend_meta.get("indexMtime", "") or ""),
            },
        )
    except Exception as e:
        return "STOPPED", {"error": str(e)}


def check_oml_server():
    """检查 oMLX 服务器（可选服务，不作为核心存活判定）。"""
    try:
        token = _omlx_token()
        headers = {"Authorization": f"Bearer {token}"} if token else {}
        data = _request_json("http://127.0.0.1:8000/v1/models", timeout=3.0, headers=headers)
        models = [m["id"] for m in data.get("data", []) if isinstance(m, dict) and m.get("id")]
        return "RUNNING", models
    except Exception as e:
        return f"STOPPED: {e}", []


def check_mt5():
    """检查 MT5 终端进程。"""
    matchers = [
        lambda name, cmd: "terminal64" in name and "metaeditor" not in name,
        lambda name, cmd: name == "terminal.exe",
    ]
    procs = []
    for proc in _iter_process_matches(matchers):
        name = str(proc.info.get("name") or "").lower()
        cmdline = _process_cmdline(proc).lower()
        if "metaeditor" in name or "metaeditor" in cmdline:
            continue
        procs.append(proc)
    procs = sorted(procs, key=lambda p: p.info.get("create_time") or 0)
    if not procs:
        return "STOPPED", {"count": 0}
    latest = procs[-1]
    return (
        "RUNNING",
        {
            "count": len(procs),
            "pid": latest.pid,
            "uptime": _format_uptime(latest.info.get("create_time") or time.time()),
            "name": latest.info.get("name", ""),
        },
    )


def check_engine(backend_data: dict | None = None):
    """检查当前交易引擎进程。"""
    matchers = [
        lambda name, cmd: "goldautotradeengine" in name,
        lambda name, cmd: "--daemon-pipeline" in cmd,
        lambda name, cmd: "gui_backend.py" in cmd,
        lambda name, cmd: "gold_trader.py" in cmd,
    ]
    procs = sorted(_iter_process_matches(matchers), key=lambda p: p.info.get("create_time") or 0)
    backend_running = bool(backend_data and backend_data.get("is_running"))
    if not procs and not backend_running:
        return "DEAD", {"count": 0}
    latest = procs[-1] if procs else None
    return (
        "RUNNING",
        {
            "count": len(procs),
            "pid": latest.pid if latest else None,
            "uptime": _format_uptime(latest.info.get("create_time") or time.time()) if latest else "",
            "backend_running": backend_running,
        },
    )


def _bridge_roots():
    roots: list[pathlib.Path] = []
    roots.append(MT5_COMMON)
    roots.append(ROOT / "bridge" / "Files" / "gold_autotrade" / "bridge")
    appdata = os.environ.get("APPDATA", "").strip()
    if appdata:
        roots.append(pathlib.Path(appdata) / "MetaQuotes" / "Terminal" / "Common" / "Files" / "gold_autotrade" / "bridge")
    seen = set()
    result = []
    for root in roots:
        key = str(root).lower()
        if key in seen:
            continue
        seen.add(key)
        result.append(root)
    return result


def _resolve_terminal_status_file():
    candidates = []
    for root in _bridge_roots():
        candidates.append(root / "terminal_status.json")
        candidates.append(root / "from_ea" / "terminal_status.json")
    existing = [path for path in candidates if path.exists()]
    if not existing:
        return None
    return max(existing, key=lambda p: p.stat().st_mtime)


def check_bridge():
    """检查 bridge 目录与 EA 状态文件。"""
    status_file = _resolve_terminal_status_file()
    status_ok = bool(status_file and status_file.exists())
    last_update = None
    file_age = None
    bid = None
    ask = None
    bridge_root = status_file.parent if status_file else MT5_COMMON
    if status_ok:
        try:
            file_age = time.time() - status_file.stat().st_mtime
            data = json.loads(status_file.read_text(encoding="utf-8"))
            last_update = data.get("updated", "?")
            bid = data.get("bid")
            ask = data.get("ask")
        except Exception:
            last_update = "PARSE_ERROR"
            file_age = None
    to_ea = bridge_root / "to_ea"
    from_ea = bridge_root / "from_ea"
    to_files = list(to_ea.glob("*.json")) if to_ea.exists() else []
    from_files = list(from_ea.glob("*.json")) if from_ea.exists() else []
    return {
        "terminal_status_exists": status_ok,
        "terminal_status_path": str(status_file) if status_file else "",
        "last_update": last_update,
        "file_age_seconds": round(file_age, 1) if file_age is not None else None,
        "pending_requests": len(to_files),
        "pending_responses": len(from_files),
        "bid": bid,
        "ask": ask,
    }


def check_gateway():
    """检查 OpenClaw Gateway（可选服务）。"""
    return "RUNNING" if _port_open(GATEWAY_PORT) else "STOPPED"


def get_system_summary():
    """生成系统健康报告。"""
    backend_status, backend_info = check_backend()
    omlx_status, omlx_models = check_oml_server()
    mt5_status, mt5_info = check_mt5()
    engine_status, engine_info = check_engine(backend_info if isinstance(backend_info, dict) else None)
    bridge_data = check_bridge()
    gateway_status = check_gateway()

    report = {
        "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "services": {
            "Backend_API": {"status": backend_status, "info": backend_info},
            "MT5_Terminal": {"status": mt5_status, "info": mt5_info},
            "Trade_Engine": {"status": engine_status, "info": engine_info},
            "OpenClaw_Gateway": {"status": gateway_status},
            "oMLX_server": {"status": omlx_status, "models": omlx_models},
        },
        "bridge": bridge_data,
        "issues": [],
        "warnings": [],
        "health_score": 100,
    }

    if backend_status != "RUNNING":
        report["issues"].append("后台接口未启动")
        report["health_score"] -= 35
    if engine_status != "RUNNING":
        report["issues"].append("交易引擎未运行")
        report["health_score"] -= 25
    if mt5_status != "RUNNING":
        report["issues"].append("MT5未运行")
        report["health_score"] -= 25
    if not bridge_data.get("terminal_status_exists"):
        report["issues"].append("MT5状态文件不存在（EA可能未加载）")
        report["health_score"] -= 15
    ea_age = bridge_data.get("file_age_seconds")
    if ea_age is not None and ea_age > 60:
        report["issues"].append(f"EA响应延迟: {ea_age:.0f}秒未更新")
        report["health_score"] -= 15
    if bridge_data.get("pending_requests", 0) > 5:
        report["warnings"].append(f"待处理请求偏多: {bridge_data['pending_requests']}")
        report["health_score"] -= 5
    if gateway_status != "RUNNING":
        report["warnings"].append("OpenClaw Gateway未运行")
    if not str(omlx_status).startswith("RUNNING"):
        report["warnings"].append("oMLX服务器未运行")

    report["health_score"] = max(0, report["health_score"])
    return report

if __name__ == "__main__":
    import json as _json
    import sys
    report = get_system_summary()

    if "--json" in sys.argv:
        print(_json.dumps(report, indent=2, ensure_ascii=False))
    else:
        print("=== 系统健康检查报告 ===")
        print(f"时间: {report['timestamp']}")
        print(f"健康评分: {report['health_score']}/100")
        print()
        for svc, data in report["services"].items():
            status = data["status"]
            icon = "[OK]" if status == "RUNNING" else "[FAIL]"
            info = data.get("info") or data.get("models") or ""
            print(f"  {icon} {svc}: {status} {info}")
        print(f"  Bridge: terminal_status={'[OK]' if report['bridge'].get('terminal_status_exists') else '[FAIL]'} | "
              f"last_update={report['bridge'].get('last_update','?')} | "
              f"pending_req={report['bridge'].get('pending_requests',0)}")
        if report["issues"]:
            print()
            print("[WARN]️  问题:")
            for issue in report["issues"]:
                print(f"  - {issue}")
        else:
            print()
            print("[OK] 所有服务运行正常")

        # 输出 JSON 格式供模型分析
        print()
        print("---JSON---")
        print(_json.dumps(report, ensure_ascii=False))

    # ── 自动修复触发 ──────────────────────────────────────
    # 当检测到 EA 无响应（K线数据不足），自动 ping + 必要时重启
    ea_age = report.get("bridge", {}).get("file_age_seconds")
    if ea_age is not None and ea_age > 60:
        print()
        print("[AUTO-HEAL] 检测到 EA 无响应，触发自动修复...")
        # 调用 health_watchdog 的 heal_ping_ea（同一目录）
        wd_script = pathlib.Path(__file__).parent / "health_watchdog.py"
        if wd_script.exists():
            from src.subprocess_util import run as _safe_run
            result = _safe_run(
                [sys.executable, str(wd_script)],
                capture_output=True, text=True, timeout=30,
                cwd=str(pathlib.Path(__file__).parent.parent),
            )
            # 打印 health_watchdog 的输出（用于日志）
            if result.stdout:
                for line in result.stdout.strip().split("\n")[-5:]:
                    print(f"  {line}")
        else:
            print("[AUTO-HEAL] health_watchdog.py 未找到，请手动检查")
