"""Tempo range helpers."""


def _validate_bpm_value(bpm: float) -> float:
    try:
        bpm_value = float(bpm)
    except (TypeError, ValueError) as exc:
        raise ValueError("bpm must be a number.") from exc

    if bpm_value <= 0:
        raise ValueError("bpm must be greater than 0.")

    return bpm_value


def _validate_range(low: float, high: float) -> tuple[float, float]:
    low_value = _validate_bpm_value(low)
    high_value = _validate_bpm_value(high)

    if low_value > high_value:
        raise ValueError("low must be less than or equal to high.")

    return low_value, high_value


def normalize_bpm(bpm: float, min_bpm: float = 70, max_bpm: float = 180) -> float:
    """Normalize a BPM value into a practical range using half/double time."""

    value = _validate_bpm_value(bpm)
    minimum, maximum = _validate_range(min_bpm, max_bpm)

    while value < minimum:
        value *= 2

    while value > maximum:
        value /= 2

    return round(value, 2)


def is_bpm_in_range(bpm: float, low: float, high: float) -> bool:
    """Return True when a BPM value is inside the provided inclusive range."""

    value = _validate_bpm_value(bpm)
    low_value, high_value = _validate_range(low, high)
    return low_value <= value <= high_value


def tempo_bucket(bpm: float) -> str:
    """Map BPM values to a small set of practical tempo labels."""

    value = _validate_bpm_value(bpm)

    if value < 60:
        return "very-slow"
    if value < 90:
        return "slow"
    if value < 120:
        return "mid-tempo"
    if value < 150:
        return "upbeat"
    return "fast"

