"""Shared utility helpers for adaptive policy modules.

Small, pure functions used by both :mod:`adaptive_online_policy` and
:mod:`adaptive_budget_online_policy`.  Kept in a single place to avoid
duplicating logic across policy controllers.
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any

import numpy as np


def utc_now() -> str:
    """Return the current UTC time as a compact ISO-8601 string."""
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def coerce_float(value: Any, *, default: float) -> float:
    """Convert *value* to float, returning *default* on failure."""
    try:
        return float(value)
    except (TypeError, ValueError):
        return float(default)


def coerce_nonnegative_int(value: Any, *, default: int) -> int:
    """Convert *value* to a non-negative int, returning *default* on failure."""
    try:
        out = int(value)
    except (TypeError, ValueError):
        return int(default)
    return max(0, out)


def safe_float(value: Any, *, default: float = 0.0) -> float:
    """Convert *value* to float, replacing NaN/Inf with *default*."""
    try:
        out = float(value)
    except (TypeError, ValueError):
        return float(default)
    if np.isnan(out) or np.isinf(out):
        return float(default)
    return out


def safe_float_tracked(value: Any, *, default: float = 0.0) -> tuple[float, bool]:
    """Like :func:`safe_float` but also report whether a replacement occurred.

    Returns a ``(result, replaced)`` tuple where *replaced* is ``True``
    when the original value was NaN, Inf, or otherwise unconvertible.
    """
    try:
        out = float(value)
    except (TypeError, ValueError):
        return float(default), True
    if np.isnan(out) or np.isinf(out):
        return float(default), True
    return out, False
