"""中控:读取 PM2 进程 stdout/stderr 日志(系统日志页)."""
from __future__ import annotations

import json
import os
import subprocess
import time
from pathlib import Path
from typing import Any

LOG_TARGETS: dict[str, dict[str, str]] = {
    "binance": {"label": "币安", "pm2_name": "crypto_binance"},
    "gate": {"label": "Gate", "pm2_name": "crypto_gate"},
    "okx": {"label": "OKX", "pm2_name": "crypto_okx"},
    "hub": {"label": "中控", "pm2_name": "manual-trading-hub"},
}

DEFAULT_LINES = 200
MAX_LINES = 500
DEFAULT_TAIL_BYTES = 400_000
_PATH_CACHE_TTL_SEC = 30

_path_cache: dict[str, tuple[Path, Path]] = {}
_path_cache_at = 0.0


def pm2_logs_dir() -> Path:
    raw = (os.getenv("PM2_HOME") or "").strip()
    base = Path(raw) if raw else Path.home() / ".pm2"
    return base / "logs"


def _pm2_jlist() -> list[dict[str, Any]]:
    try:
        proc = subprocess.run(
            ["pm2", "jlist"],
            capture_output=True,
            text=True,
            timeout=8,
            check=False,
        )
        if proc.returncode != 0:
            return []
        data = json.loads(proc.stdout or "[]")
        return data if isinstance(data, list) else []
    except (OSError, subprocess.SubprocessError, json.JSONDecodeError, ValueError):
        return []


def _newest_match(pattern: str, logs_dir: Path) -> Path | None:
    matches = [p for p in logs_dir.glob(pattern) if p.is_file()]
    if not matches:
        return None
    return max(matches, key=lambda p: p.stat().st_mtime)


def _glob_log_paths(pm2_name: str) -> tuple[Path, Path]:
    logs_dir = pm2_logs_dir()
    out_path = Path()
    err_path = Path()
    if not logs_dir.is_dir():
        return out_path, err_path
    slugs = {pm2_name, pm2_name.replace("_", "-")}
    for slug in slugs:
        if not out_path.is_file():
            hit = _newest_match(f"{slug}-out*.log", logs_dir)
            if hit is not None:
                out_path = hit
        if not err_path.is_file():
            hit = _newest_match(f"{slug}-error*.log", logs_dir)
            if hit is not None:
                err_path = hit
    return out_path, err_path


def resolve_log_paths(pm2_name: str) -> tuple[Path, Path]:
    global _path_cache_at
    now = time.time()
    if now - _path_cache_at > _PATH_CACHE_TTL_SEC:
        _path_cache.clear()
        _path_cache_at = now
    cached = _path_cache.get(pm2_name)
    if cached is not None:
        return cached

    out_path = Path()
    err_path = Path()
    for proc in _pm2_jlist():
        if proc.get("name") != pm2_name:
            continue
        env = proc.get("pm2_env") or {}
        raw_out = (env.get("pm_out_log_path") or "").strip()
        raw_err = (env.get("pm_err_log_path") or "").strip()
        if raw_out:
            out_path = Path(raw_out)
        if raw_err:
            err_path = Path(raw_err)
        break

    glob_out, glob_err = _glob_log_paths(pm2_name)
    if not out_path.is_file() and glob_out.is_file():
        out_path = glob_out
    if not err_path.is_file() and glob_err.is_file():
        err_path = glob_err

    result = (out_path, err_path)
    _path_cache[pm2_name] = result
    return result


def log_file_paths(pm2_name: str) -> tuple[Path, Path]:
    return resolve_log_paths(pm2_name)


def tail_lines(
    path: Path,
    lines: int = DEFAULT_LINES,
    *,
    max_bytes: int = DEFAULT_TAIL_BYTES,
) -> str:
    if not path.is_file():
        return ""
    try:
        size = path.stat().st_size
        with path.open("rb") as handle:
            if size <= max_bytes:
                data = handle.read()
            else:
                handle.seek(max(0, size - max_bytes))
                data = handle.read()
        text = data.decode("utf-8", errors="replace")
        parts = text.splitlines()
        if len(parts) > lines:
            parts = parts[-lines:]
        return "\n".join(parts)
    except OSError:
        return ""


def system_logs_meta() -> dict[str, Any]:
    return {
        "ok": True,
        "targets": [
            {"key": key, "label": cfg["label"], "pm2_name": cfg["pm2_name"]}
            for key, cfg in LOG_TARGETS.items()
        ],
        "default_lines": DEFAULT_LINES,
        "max_lines": MAX_LINES,
    }


def load_system_logs(target: str, lines: int = DEFAULT_LINES) -> dict[str, Any]:
    key = (target or "").strip().lower()
    if key not in LOG_TARGETS:
        raise KeyError(key)
    cfg = LOG_TARGETS[key]
    line_count = max(20, min(MAX_LINES, int(lines or DEFAULT_LINES)))
    out_path, err_path = resolve_log_paths(cfg["pm2_name"])
    return {
        "ok": True,
        "key": key,
        "label": cfg["label"],
        "pm2_name": cfg["pm2_name"],
        "lines": line_count,
        "out": tail_lines(out_path, line_count),
        "err": tail_lines(err_path, line_count),
        "out_exists": out_path.is_file(),
        "err_exists": err_path.is_file(),
        "out_path": str(out_path) if str(out_path) else "",
        "err_path": str(err_path) if str(err_path) else "",
        "updated_at": int(time.time()),
    }
