#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
请求级运行指标（轻量内存版）
- 记录请求总量、状态分布、耗时
- 记录慢请求样本（环形队列）
- 输出健康检查可读快照
"""

from __future__ import annotations

import os
import threading
import time
from collections import deque
from typing import Dict

_SLOW_REQUEST_MS = max(0.0, float(os.environ.get("REQUEST_METRICS_SLOW_MS", "1200") or 1200))
_PATH_STATS_LIMIT = max(20, int(os.environ.get("REQUEST_METRICS_PATH_LIMIT", "120") or 120))
_RECENT_SLOW_LIMIT = max(10, int(os.environ.get("REQUEST_METRICS_RECENT_SLOW_LIMIT", "20") or 20))

_lock = threading.Lock()

_state = {
    "start_ts": time.time(),
    "last_request_ts": 0.0,
    "total_requests": 0,
    "api_requests": 0,
    "error_requests": 0,
    "slow_requests": 0,
    "duration_total_ms": 0.0,
    "max_duration_ms": 0.0,
    "status_counts": {},
    "path_stats": {},
    "recent_slow": deque(maxlen=_RECENT_SLOW_LIMIT),
}


def _status_bucket(status_code: int) -> str:
    code = int(status_code or 0)
    if code <= 0:
        return "unknown"
    head = code // 100
    if head in (1, 2, 3, 4, 5):
        return f"{head}xx"
    return str(code)


def _normalize_path(path: str) -> str:
    text = str(path or "/")
    if len(text) > 160:
        return f"{text[:157]}..."
    return text


def _prune_path_stats(path_stats: Dict[str, dict]) -> None:
    if len(path_stats) < _PATH_STATS_LIMIT:
        return

    # 删除最不活跃的路径，避免无限增长
    removable_key = None
    removable_score = None
    for key, item in path_stats.items():
        count = int(item.get("count", 0) or 0)
        max_ms = float(item.get("max_ms", 0.0) or 0.0)
        score = (count, max_ms)
        if removable_score is None or score < removable_score:
            removable_key = key
            removable_score = score

    if removable_key:
        path_stats.pop(removable_key, None)


def record_request_metric(*, path: str, method: str, status_code: int, duration_ms: float, is_api: bool = False) -> None:
    duration = max(0.0, float(duration_ms or 0.0))
    code = int(status_code or 0)
    method_name = str(method or "GET").upper()
    normalized_path = _normalize_path(path)
    route_key = f"{method_name} {normalized_path}"
    now = time.time()

    with _lock:
        _state["total_requests"] += 1
        if is_api:
            _state["api_requests"] += 1
        if code >= 500:
            _state["error_requests"] += 1

        _state["last_request_ts"] = now
        _state["duration_total_ms"] += duration
        if duration > _state["max_duration_ms"]:
            _state["max_duration_ms"] = duration

        bucket = _status_bucket(code)
        status_counts = _state["status_counts"]
        status_counts[bucket] = int(status_counts.get(bucket, 0) or 0) + 1

        path_stats = _state["path_stats"]
        if route_key not in path_stats:
            _prune_path_stats(path_stats)
            path_stats[route_key] = {
                "count": 0,
                "total_ms": 0.0,
                "max_ms": 0.0,
                "status_5xx": 0,
            }

        item = path_stats[route_key]
        item["count"] = int(item.get("count", 0) or 0) + 1
        item["total_ms"] = float(item.get("total_ms", 0.0) or 0.0) + duration
        if duration > float(item.get("max_ms", 0.0) or 0.0):
            item["max_ms"] = duration
        if code >= 500:
            item["status_5xx"] = int(item.get("status_5xx", 0) or 0) + 1

        if _SLOW_REQUEST_MS > 0 and duration >= _SLOW_REQUEST_MS:
            _state["slow_requests"] += 1
            _state["recent_slow"].append(
                {
                    "path": normalized_path,
                    "method": method_name,
                    "status": code,
                    "duration_ms": round(duration, 2),
                    "time": int(now),
                }
            )


def get_request_metrics_snapshot() -> dict:
    with _lock:
        total_requests = int(_state["total_requests"])
        duration_total_ms = float(_state["duration_total_ms"])
        avg_duration_ms = round((duration_total_ms / total_requests), 2) if total_requests > 0 else 0.0

        path_rows = []
        for key, item in _state["path_stats"].items():
            count = int(item.get("count", 0) or 0)
            total_ms = float(item.get("total_ms", 0.0) or 0.0)
            avg_ms = round((total_ms / count), 2) if count > 0 else 0.0
            path_rows.append(
                {
                    "path": key,
                    "count": count,
                    "avg_ms": avg_ms,
                    "max_ms": round(float(item.get("max_ms", 0.0) or 0.0), 2),
                    "status_5xx": int(item.get("status_5xx", 0) or 0),
                }
            )

        top_paths = sorted(
            path_rows,
            key=lambda row: (float(row.get("max_ms", 0.0)), float(row.get("avg_ms", 0.0)), int(row.get("count", 0))),
            reverse=True,
        )[:8]

        return {
            "since_ts": int(_state["start_ts"]),
            "uptime_seconds": max(0, int(time.time() - float(_state["start_ts"]))),
            "last_request_ts": int(_state["last_request_ts"] or 0),
            "total_requests": total_requests,
            "api_requests": int(_state["api_requests"]),
            "error_requests": int(_state["error_requests"]),
            "slow_requests": int(_state["slow_requests"]),
            "avg_duration_ms": avg_duration_ms,
            "max_duration_ms": round(float(_state["max_duration_ms"]), 2),
            "status_counts": dict(_state["status_counts"]),
            "top_paths": top_paths,
            "recent_slow": list(_state["recent_slow"]),
            "slow_threshold_ms": _SLOW_REQUEST_MS,
        }
