import math
from typing import Iterable, List, Optional


# Numerical Safety
def safe_divide(a: float, b: float, default: float = 0.0) -> float:
    """
    Divide a by b, returning default if b == 0.
    """
    return a / b if b != 0 else default


def is_finite(x: float) -> bool:
    """
    Return True if x is a finite number.
    """
    return math.isfinite(x)


def clip(
    x: float,
    lower: Optional[float] = None,
    upper: Optional[float] = None,
) -> float:
    """
    Clip x to [lower, upper] if bounds are provided.
    """
    if lower is not None:
        x = max(x, lower)
    if upper is not None:
        x = min(x, upper)
    return x


# Transformations
def log_safe(x: float, default: float = 0.0) -> float:
    """
    Natural log with guard for non-positive values.
    """
    return math.log(x) if x > 0 else default


def zscore(
    x: float,
    mean: float,
    std: float,
    default: float = 0.0,
) -> float:
    """
    Compute z-score with zero-division protection.
    """
    return safe_divide(x - mean, std, default)


# Aggregations
def mean(values: Iterable[float]) -> float:
    values = list(values)
    if not values:
        return 0.0
    return sum(values) / len(values)


def variance(values: Iterable[float]) -> float:
    values = list(values)
    if len(values) < 2:
        return 0.0
    m = mean(values)
    return sum((x - m) ** 2 for x in values) / (len(values) - 1)


def std(values: Iterable[float]) -> float:
    return math.sqrt(variance(values))


def weighted_average(
    values: Iterable[float],
    weights: Iterable[float],
    default: float = 0.0,
) -> float:
    values = list(values)
    weights = list(weights)

    if not values or not weights or len(values) != len(weights):
        return default

    total_weight = sum(weights)
    if total_weight == 0:
        return default

    return sum(v * w for v, w in zip(values, weights)) / total_weight


def normalize(weights: Iterable[float]) -> List[float]:
    weights = list(weights)
    total = sum(weights)

    if total == 0:
        return [0.0 for _ in weights]

    return [w / total for w in weights]


# Rolling Windows
def rolling_mean(values: Iterable[float], window: int) -> List[float]:
    """
    Compute rolling mean over a sequence.
    """
    values = list(values)
    if window <= 0:
        raise ValueError("window must be positive")

    return [
        mean(values[i : i + window])
        for i in range(len(values) - window + 1)
    ]