"""Every outside service the application asks -- one source of truth for
their limits, how they count, their licences, what they require, the
thresholds and what the application gives up at each (AV-052; started in
AV-063, Open-Meteo and MET Norway added in AV-051).

Each rule names where it comes from and when it was checked. Where a service
publishes no number (fair use), ``daily_budget`` is our own ceiling: the
thresholds (budgets.py) are shares of it.

``side``: "server" -- the request leaves this server through
mountain_twin.offline.urlopen and is counted (usage.py); "browser" -- the
page asks it directly (map tiles, the radar's images, an embedded frame):
not counted here, its rules are kept by the page's own pacing and caching.

Production and development are separate servers with separate IP addresses,
so separate free limits: the development server never uses production's
keys or endpoints with a limit (MAPTILER_API_KEY, LIVE_TRACKING_ADMIN_SECRET
and the like live in each machine's own environment, never in the code).
"""

from __future__ import annotations

from urllib.parse import urlsplit

# The default thresholds -- a share of the day's limit (or budget).
THRESHOLDS = {"YELLOW": 0.5, "RED": 0.8, "STOP": 0.95}

# What each level gives up (budgets.py enforces the machine-readable part):
# YELLOW -- no prefetch, the expensive features from the cache only;
# RED    -- cache and fallback sources only, new requests only for actions
#           the user asked for, nothing in the background;
# STOP   -- no request at all until the limit renews.
DEFAULT_DEGRADATION = {
    "YELLOW": "background prefetch off; expensive features (model comparison) from the cache only",
    "RED": "cache and fallback sources only; requests only for what the user asked for; nothing in the background",
    "STOP": "no request until the limit renews; the page says when",
}

SERVICES: dict[str, dict] = {
    "open_meteo": {
        "name": "Open-Meteo",
        "hosts": [
            "api.open-meteo.com",
            "historical-forecast-api.open-meteo.com",
            "archive-api.open-meteo.com",
        ],
        "side": "server",
        "used_for": "the weather of Journeys and places, the model comparison, Storms",
        "limits": {"per_minute": 600, "per_hour": 5000, "per_day": 10000, "per_month": 300000},
        "counting": "weight: per location and model, x(variables/10) over 10 variables, "
        "x(days/14) over 14 days (mountain_twin.weather.forecast_cache.call_weight)",
        "daily_budget": 10000,
        "licence": "data CC BY 4.0; the free API for non-commercial use only -- commercial use "
        "needs a paid plan",
        "requirements": "attribution 'Weather data by Open-Meteo.com'; cache answers",
        "rules_source": "https://open-meteo.com/en/terms and /en/pricing (checked 2026-10-05)",
        "fallback": "met_norway",
        "client": "mountain_twin.weather.live.LiveWeatherPointResolver",
        "attribution": "Weather data by Open-Meteo.com (CC BY 4.0)",
    },
    "met_norway": {
        "name": "MET Norway Locationforecast 2.0",
        "hosts": ["api.met.no"],
        "side": "server",
        "used_for": "the second weather source when Open-Meteo refuses or reaches RED "
        "(WEATHER_PRIMARY=met_norway: first)",
        "limits": {"per_second": 20},
        "counting": "requests (one point each)",
        "daily_budget": 20000,  # our ceiling: no published daily number
        "licence": "CC BY 4.0 (commercial use allowed with attribution)",
        "requirements": "identifying User-Agent with a contact (MET_NORWAY_CONTACT); cache and "
        "revalidate (Expires, If-Modified-Since); at most 4 decimals; <= 20 requests/s in "
        "all; gzip; HTTPS",
        "rules_source": "https://api.met.no/doc/TermsOfService (checked 2026-10-05)",
        "client": "mountain_twin.weather.met_norway.MetNorwayClient",
        "attribution": "Dane: MET Norway (CC BY 4.0)",
    },
    "photon": {
        "name": "Photon (komoot)",
        "hosts": ["photon.komoot.io"],
        "side": "server",
        "used_for": "Pogoda: place search (mountain_twin/places/search.py); the Planning "
        "Workspace's place search also asks it from the browser",
        "limits": {},
        "counting": "requests",
        "daily_budget": 2000,  # our ceiling for a free public instance (fair use)
        "licence": "data ODbL (OpenStreetMap); the public instance is free for fair use",
        "requirements": "no heavy use; cache answers; name the application; the server asks at "
        "most once a second and keeps answers 24 h",
        "rules_source": "https://photon.komoot.io/ (checked 2026-10-05)",
        "client": "mountain_twin.places.search.PhotonPlaceSearch",
        "attribution": "© OpenStreetMap contributors",
    },
    "nominatim": {
        "name": "Nominatim (OSM Foundation)",
        "hosts": ["nominatim.openstreetmap.org"],
        "side": "server",
        "used_for": "Pogoda: a peak's own OSM height (ele) and importance by its OSM object "
        "(mountain_twin/places/osm_tags.py)",
        "limits": {"per_second": 1},
        "counting": "requests (up to 50 objects each)",
        "daily_budget": 1000,  # our ceiling: lookups for searches people made, cached for good
        "licence": "data ODbL (OpenStreetMap)",
        "requirements": "at most 1 request per second; a valid User-Agent naming the "
        "application; results cached (kept for good here); no bulk or systematic queries -- "
        "only the objects of a search someone made",
        "rules_source": "https://operations.osmfoundation.org/policies/nominatim/ "
        "(checked 2026-10-05)",
        "client": "mountain_twin.places.osm_tags.OsmTagLookup",
        "attribution": "© OpenStreetMap contributors (ODbL)",
    },
    "rainviewer": {
        "name": "RainViewer",
        "hosts": ["api.rainviewer.com", "tilecache.rainviewer.com"],
        "side": "server",
        "used_for": "the Storms tab's radar: the frame index from the server (cached ~5 min), "
        "the tiles from the browser",
        "limits": {},
        "counting": "requests (the index)",
        "daily_budget": 1000,
        "licence": "free API with attribution; RainViewer asks commercial users to contact them",
        "requirements": "attribution and a link to RainViewer; AVENTURRO_RADAR=off turns it off "
        "before a commercial release",
        "rules_source": "https://www.rainviewer.com/api.html (checked 2026-10-05)",
        "client": "mountain_twin.weather.radar.RainViewerRadarProvider",
        "attribution": "RainViewer",
    },
    "eumetsat": {
        "name": "EUMETSAT View (WMS)",
        "hosts": ["view.eumetsat.int"],
        "side": "server",
        "used_for": "the Storms tab's cloud imagery: the layers' capabilities from the server, "
        "the images from the browser",
        "limits": {},
        "counting": "requests (capabilities)",
        "daily_budget": 1000,
        "licence": "EUMETSAT data policy: free with attribution",
        "requirements": "attribution '© EUMETSAT'",
        "rules_source": "https://www.eumetsat.int/data-policy (checked 2026-10-05)",
        "client": "mountain_twin.weather.clouds.EumetsatCloudProvider",
        "attribution": "© EUMETSAT",
    },
    "copernicus_dem": {
        "name": "Copernicus DEM GLO-90 (AWS open data)",
        "hosts": ["copernicus-dem-90m.s3.amazonaws.com"],
        "side": "server",
        "used_for": "the 3D view's terrain, the heights of places without an OSM height",
        "limits": {},
        "counting": "requests (one 1x1 degree tile, ~5 MB, kept on disk for good)",
        "daily_budget": 200,
        "licence": "Copernicus DEM licence: free, attribution",
        "requirements": "attribution; tiles kept on disk (each is downloaded once)",
        "rules_source": "https://registry.opendata.aws/copernicus-dem/ (checked 2026-10-05)",
        "client": "mountain_twin.terrain.copernicus.CopernicusGlo90",
        "attribution": "Copernicus DEM © DLR / Airbus (ESA)",
    },
    "brouter_public": {
        "name": "BRouter public (brouter.de)",
        "hosts": ["brouter.de"],
        "side": "server",
        "used_for": "route drawing when USE_OWN_SERVER=0 (the default is the own server)",
        "limits": {},
        "counting": "requests (one segment each)",
        "daily_budget": 2000,
        "licence": "free public instance, fair use; data ODbL",
        "requirements": "fair use: the own BRouter is the default",
        "rules_source": "https://brouter.de/ (checked 2026-10-05)",
        "client": "mountain_twin.journey.route_provider",
        "attribution": "© OpenStreetMap contributors",
    },
    "own_server": {
        "name": "Own server (OVH): tiles, BRouter, live tracking",
        "hosts": ["57.131.197.101"],
        "side": "server",
        "used_for": "routing, the basemap/terrain tiles (browser), the live-tracking links",
        "limits": {},
        "counting": "requests",
        "daily_budget": 100000,  # our own machine: a health ceiling, not a quota
        "licence": "own",
        "requirements": "its own capacity (docs/design_reference/self_hosted_server_v0_1.md)",
        "rules_source": "infra/self_hosted/",
        "client": "mountain_twin.journey.route_provider, mountain_twin.live_tracking.owner",
        "attribution": "© OpenStreetMap contributors, Mapterhorn, Protomaps",
    },
    "strava": {
        "name": "Strava API",
        "hosts": ["www.strava.com"],
        "side": "server",
        "used_for": "the owner's activity import (manual scripts)",
        "limits": {"per_15_minutes": 100, "per_day": 1000},
        "counting": "requests",
        "daily_budget": 1000,
        "licence": "Strava API agreement: data for its owner only, no AI/ML use",
        "requirements": "OAuth token per user; rate limits per application",
        "rules_source": "https://developers.strava.com/docs/rate-limits/ (checked 2026-10-05)",
        "client": "mountain_twin.activity.strava.client",
        "attribution": "Powered by Strava",
    },
    "maptiler": {
        "name": "MapTiler (optional basemap)",
        "hosts": ["api.maptiler.com"],
        "side": "browser",
        "used_for": "MAP_BASEMAP=maptiler only (the own server is the default)",
        "limits": {"per_month": 100000},
        "counting": "map loads / tiles (MapTiler's own counter)",
        "daily_budget": 3000,
        "licence": "free plan non-commercial; key per machine",
        "requirements": "the key never in the code or the tests (AV-028)",
        "rules_source": "https://www.maptiler.com/cloud/pricing/ (checked 2026-10-05)",
        "client": "visual_prototype/map_provider.js",
        "attribution": "© MapTiler © OpenStreetMap contributors",
    },
    "ntfy": {
        "name": "ntfy (ntfy.sh)",
        "hosts": ["ntfy.sh"],
        "side": "server",
        "used_for": "the owner's usage alarms on the production server (ALARM_CHANNEL=ntfy, "
        "a secret NTFY_TOPIC; mountain_twin/usage_alarms.py)",
        "limits": {},
        "counting": "messages (at most one per service, alarm kind and UTC day)",
        "daily_budget": 50,  # our ceiling: a handful of alarms a day at most
        "licence": "the public instance is free with per-visitor rate limits; open source "
        "(Apache 2.0 / GPLv2), can run on the own server (NTFY_SERVER)",
        "requirements": "the topic name is the only secret: random, kept only in the server's "
        "environment; no personal data or routes in a message -- only service names and numbers",
        "rules_source": "https://docs.ntfy.sh/publish/ (checked 2026-10-05)",
        "client": "mountain_twin.usage_alarms.ntfy_notifier",
        "attribution": "",
    },
    "blitzortung": {
        "name": "Blitzortung.org live map",
        "hosts": ["map.blitzortung.org", "www.blitzortung.org"],
        "side": "browser",
        "used_for": "the Storms tab's lightning frame (embedded with the authors' consent)",
        "limits": {},
        "counting": "page views (not counted here)",
        "daily_budget": None,
        "licence": "personal use; not for protection of people or property",
        "requirements": "their own frame and credit, unchanged",
        "rules_source": "https://www.blitzortung.org/ (checked 2026-10-05)",
        "client": "aventurro/index.html",
        "attribution": "Blitzortung.org and contributors",
    },
    "unpkg": {
        "name": "unpkg (CDN for the page's libraries)",
        "hosts": ["unpkg.com"],
        "side": "browser",
        "used_for": "Leaflet, MapLibre and plugins, pinned versions with integrity",
        "limits": {},
        "counting": "page loads (not counted here)",
        "daily_budget": None,
        "licence": "the libraries' own (BSD/MIT)",
        "requirements": "pinned versions, SRI integrity",
        "rules_source": "https://unpkg.com/ (checked 2026-10-05)",
        "client": "aventurro/index.html",
        "attribution": "",
    },
}

_BY_HOST = {host: key for key, service in SERVICES.items() for host in service["hosts"]}


def service_for_host(host: str) -> str:
    """The service a request's host belongs to ("other" when unknown)."""
    host = (host or "").lower()
    if host in _BY_HOST:
        return _BY_HOST[host]
    for known, key in _BY_HOST.items():
        if host.endswith("." + known):
            return key
    return "other"


def service_for_url(url: str) -> str:
    return service_for_host(urlsplit(url).hostname or "")


def thresholds(service: str) -> dict[str, float]:
    return {**THRESHOLDS, **SERVICES.get(service, {}).get("thresholds", {})}
