"""common/config_client.py — the RECEIVER side of the central config service.

STDLIB-ONLY (urllib), mirroring andrea_svc_auth/client.py, so the lean pool_worker
baked into every worker image carries NO extra deps. A receiver calls
`fetch_config(service, box)` each settle-cycle to pull the merged {k: v} for its
service+box and re-read live-tuned params WITHOUT a redeploy.

FAIL-SOFT BY CONTRACT: a config fetch must NEVER take down the receiver. On ANY
exception / timeout / non-200 / bad JSON, `fetch_config` returns {} (never raises).
The caller then falls back to its baked-in env/default — so a config-service outage
degrades to today's static behaviour, not an outage. This is the inverse of
andrea_svc_auth/client.post_event (which raises and lets the caller decide): here the
swallow is baked in because the call site is hot and best-effort by design.

Auth: the X-Agent-Key header is read from env ANDREA_SVC_API_KEY (the same key the
rest of the stack mints; the service validates it against ANDREA_SVC_KEYS).
"""
from __future__ import annotations

import json
import os
import urllib.error
import urllib.parse
import urllib.request
from typing import Iterable, Optional

API_KEY_ENV = "ANDREA_SVC_API_KEY"
URL_ENV = "CONFIG_SERVICE_URL"
DEFAULT_URL = "http://10.121.1.1:9079"
API_KEY_HEADER = "X-Agent-Key"


def _base_url() -> str:
    return (os.getenv(URL_ENV) or DEFAULT_URL).rstrip("/")


def fetch_config(service: str, box: Optional[str] = None, *, timeout: float = 5) -> dict:
    """GET {CONFIG_SERVICE_URL}/config/{service}(?box=) with X-Agent-Key, return the
    merged {k: v}. FAIL-SOFT: any error / non-200 / bad JSON -> {} (never raises).

    `box` (the host) overlays box-specific knobs on top of the global '*' defaults
    server-side; omit it to get only the global defaults.
    """
    try:
        url = f"{_base_url()}/config/{urllib.parse.quote(str(service))}"
        if box:
            url += "?" + urllib.parse.urlencode({"box": box})
        headers = {}
        key = os.getenv(API_KEY_ENV, "")
        if key:
            headers[API_KEY_HEADER] = key
        req = urllib.request.Request(url, method="GET", headers=headers)
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            status = getattr(resp, "status", None)
            if status is None:
                status = resp.getcode()
            if status != 200:
                return {}
            data = resp.read()
        parsed = json.loads(data)
        if not isinstance(parsed, dict):
            return {}
        return parsed
    except Exception:                              # noqa: BLE001 — fail-soft by contract
        return {}


def layer_env(service: str, keys: Iterable[str], box: Optional[str] = None,
              *, timeout: float = 5) -> dict:
    """Fetch the service config and return ONLY the requested keys that are present.

    Lets a caller layer live config over its env/defaults cleanly:

        cfg = layer_env("compressor", ["LOAD_GATE", "POLL_MIN"], box="w2295")
        load_gate = cfg.get("LOAD_GATE", os.getenv("LOAD_GATE", "8.0"))

    Empty on any fetch failure (fail-soft inherited from fetch_config), so the
    caller's `.get(key, default)` falls through to the baked-in default.
    """
    cfg = fetch_config(service, box, timeout=timeout)
    wanted = set(keys)
    return {k: v for k, v in cfg.items() if k in wanted}
