"""Thresholds and automatic degradation of the outside services (AV-052).

A service's level is the share of its day's limit (or our ``daily_budget``,
mountain_twin.external_services) used today -- Open-Meteo also its hourly
limit, whichever is further: GREEN below YELLOW (50 %), YELLOW, RED (80 %),
STOP (95 %). What each level gives up:

* GREEN  -- everything;
* YELLOW -- no background work (prefetch, background refresh); the
  expensive features (the model comparison) from the cache only;
* RED    -- what the user asked for only, and where a fallback source
  exists (the weather: MET Norway) that one instead; nothing in the
  background;
* STOP   -- no request at all until the limit renews (UTC midnight).

``check`` is called by mountain_twin.offline.urlopen before every request,
so nothing goes around it; it raises ServiceBudgetExceeded (a RuntimeError,
which every client already treats as "no answer"), and the weather resolver
turns it into its own reason (SERVICE_BUDGET_YELLOW/RED/STOP) and, at RED,
the fallback.
"""

from __future__ import annotations

from datetime import datetime, timedelta, timezone

from mountain_twin import usage
from mountain_twin.external_services import SERVICES, thresholds

LEVELS = ("GREEN", "YELLOW", "RED", "STOP")


class ServiceBudgetExceeded(RuntimeError):
    def __init__(self, service: str, level: str, feature: str):
        super().__init__(f"SERVICE_BUDGET_{level}")
        self.service, self.level, self.feature = service, level, feature
        self.reason_code = f"SERVICE_BUDGET_{level}"


def share(service: str) -> float:
    """The larger of the day's and (where there is one) the hour's share."""
    store = usage.store()
    config = SERVICES.get(service)
    if store is None or not config or not config.get("daily_budget"):
        return 0.0
    shares = [store.today(service)["weight"] / config["daily_budget"]]
    per_hour = config.get("limits", {}).get("per_hour")
    if per_hour:
        shares.append(store.this_hour(service)["weight"] / per_hour)
    return max(shares)


def level(service: str) -> str:
    used, limits = share(service), thresholds(service)
    if used >= limits["STOP"]:
        return "STOP"
    if used >= limits["RED"]:
        return "RED"
    if used >= limits["YELLOW"]:
        return "YELLOW"
    return "GREEN"


def allowed(service: str, feature: str, current: str | None = None) -> bool:
    current = current or level(service)
    if current == "GREEN":
        return True
    if current == "STOP":
        return False
    if feature in usage.BACKGROUND_FEATURES:
        return False
    if current == "YELLOW":
        return feature not in usage.EXPENSIVE_FEATURES
    # RED: the user's own actions only -- and a service with a fallback
    # leaves even those to it (the weather: MET Norway).
    if feature in usage.EXPENSIVE_FEATURES:
        return False
    return not SERVICES.get(service, {}).get("fallback")


def check(service: str) -> None:
    """Before a request: raises ServiceBudgetExceeded when its level forbids
    this feature."""
    if usage.store() is None or service not in SERVICES:
        return
    feature, current = usage.current_feature(service), level(service)
    if not allowed(service, feature, current):
        raise ServiceBudgetExceeded(service, current, feature)


def renews_at(now: datetime | None = None) -> str:
    """The day's limits renew at UTC midnight."""
    moment = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
    return (
        moment.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
    ).isoformat()


def state_document() -> dict:
    """Every server-side service's level, for the page (it shows only
    YELLOW and above) and the usage panel."""
    services = {}
    for key, config in SERVICES.items():
        if config["side"] != "server" or not config.get("daily_budget"):
            continue
        services[key] = {
            "name": config["name"],
            "level": level(key),
            "share": round(share(key), 4),
            "fallback": config.get("fallback"),
        }
    return {"services": services, "renews_at": renews_at(), "thresholds": dict(thresholds("x"))}
