import math
import threading
from datetime import datetime
from typing import Dict, List, Tuple

# edited by glg


class ExportRuntimeMetricsService:
    def __init__(self, max_samples: int = 64):
        self._max_samples = max(3, int(max_samples or 64))
        self._lock = threading.Lock()
        self._latency_samples: List[float] = []
        self._attempt_samples: List[Tuple[int, int]] = []
        self._updated_at = ""

    @staticmethod
    def _to_int(value, default=0) -> int:
        try:
            return int(value)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return int(default)

    @staticmethod
    def _to_float(value, default=0.0) -> float:
        try:
            return float(value)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return float(default)

    @staticmethod
    def _p95(values: List[float]) -> float:
        nums = [float(v) for v in (values or []) if float(v) >= 0.0]
        if not nums:
            return 0.0
        nums.sort()
        rank = max(0, min(len(nums) - 1, int(math.ceil(len(nums) * 0.95) - 1)))
        return float(nums[rank])

    def observe_cycle(self, result: Dict, elapsed_ms: float) -> None:
        payload = result if isinstance(result, dict) else {}
        ran = bool(payload.get("ran"))
        if not ran:
            return

        uploaded = max(0, self._to_int(payload.get("uploaded"), 0))
        failed = max(0, self._to_int(payload.get("failed"), 0))
        attempts = uploaded + failed
        latency = max(0.0, self._to_float(elapsed_ms, 0.0))

        with self._lock:
            self._latency_samples.append(latency)
            if len(self._latency_samples) > self._max_samples:
                self._latency_samples = self._latency_samples[-self._max_samples :]

            if attempts > 0:
                self._attempt_samples.append((failed, attempts))
                if len(self._attempt_samples) > self._max_samples:
                    self._attempt_samples = self._attempt_samples[-self._max_samples :]

            self._updated_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    def snapshot(self) -> Dict:
        with self._lock:
            latency_samples = list(self._latency_samples)
            attempt_samples = list(self._attempt_samples)
            updated_at = str(self._updated_at or "")

        failed_total = int(sum(item[0] for item in attempt_samples))
        attempts_total = int(sum(item[1] for item in attempt_samples))
        error_rate = (float(failed_total) * 100.0 / float(attempts_total)) if attempts_total > 0 else 0.0
        avg_latency = (
            float(sum(latency_samples)) / float(len(latency_samples))
            if latency_samples
            else 0.0
        )
        return {
            "error_rate_pct": float(round(error_rate, 4)),
            "p95_latency_ms": float(round(self._p95(latency_samples), 3)),
            "avg_latency_ms": float(round(avg_latency, 3)),
            "latency_sample_count": int(len(latency_samples)),
            "attempt_sample_count": int(len(attempt_samples)),
            "updated_at": updated_at,
        }
