"""Quiet hours — when the pet is allowed to make noise on its own.

Napping only ever suppresses *proactive* noise (heartbeat announcements,
forwarded notifications) and wandering. The wake word, click-to-talk and
push-to-talk still work: telling it to be quiet shouldn't mean it stops
answering when spoken to.

Pure logic (parsing + a time comparison) so it's testable without waiting
for 11pm.
"""

from __future__ import annotations

from datetime import time as dtime
from typing import Iterable, Optional


class QuietHoursError(ValueError):
    pass


def _parse_clock(value: str) -> dtime:
    parts = value.strip().split(":")
    if len(parts) != 2:
        raise QuietHoursError(f"expected HH:MM, got {value!r}")
    try:
        hour, minute = int(parts[0]), int(parts[1])
    except ValueError as exc:
        raise QuietHoursError(f"expected HH:MM, got {value!r}") from exc
    if not (0 <= hour <= 23 and 0 <= minute <= 59):
        raise QuietHoursError(f"{value!r} is not a real time of day")
    return dtime(hour, minute)


def parse_ranges(spec: str) -> list[tuple[dtime, dtime]]:
    """"23:00-08:00, 13:00-14:00" -> [(23:00, 08:00), (13:00, 14:00)].
    Empty/blank spec means "no quiet hours"."""
    ranges: list[tuple[dtime, dtime]] = []
    for chunk in (spec or "").split(","):
        chunk = chunk.strip()
        if not chunk:
            continue
        start, sep, end = chunk.partition("-")
        if not sep:
            raise QuietHoursError(f"expected HH:MM-HH:MM, got {chunk!r}")
        ranges.append((_parse_clock(start), _parse_clock(end)))
    return ranges


def in_ranges(now: dtime, ranges: Iterable[tuple[dtime, dtime]]) -> bool:
    for start, end in ranges:
        if start == end:
            continue  # a zero-length range is a typo, not "all day"
        if start < end:
            if start <= now < end:
                return True
        elif now >= start or now < end:  # wraps past midnight
            return True
    return False


def is_quiet(spec: str, now: Optional[dtime] = None, on_error=None) -> bool:
    """True if *now* (defaults to the local wall clock) falls inside *spec*.
    A malformed spec is reported via *on_error* and treated as "not quiet" —
    a config typo shouldn't silently mute the pet forever."""
    if not (spec or "").strip():
        return False
    try:
        ranges = parse_ranges(spec)
    except QuietHoursError as exc:
        if on_error is not None:
            on_error(exc)
        return False
    if now is None:
        from datetime import datetime

        now = datetime.now().time()
    return in_ranges(now, ranges)
