"""
Indie Flags client. Uses GET /eval and GET /eval/flags.
Works with Python 3.9+ (stdlib only).
"""

import json
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Optional


DEFAULT_BASE_URL = "https://api.indieflags.dev"
CACHE_TTL_SEC = 60


class IndieFlags:
    """
    Feature flags client for Indie Flags API.
    """

    def __init__(
        self,
        api_key: str,
        *,
        base_url: Optional[str] = None,
        env: str = "production",
        email: Optional[str] = None,
        cache_ttl_sec: int = CACHE_TTL_SEC,
    ):
        self._api_key = api_key.strip()
        if not self._api_key:
            raise ValueError("IndieFlags: api_key is required")
        self._base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
        self._env = (env or "production").strip().lower() or "production"
        self._email = email.strip() if email else None
        self._cache_ttl_sec = cache_ttl_sec
        self._cache: Optional[dict[str, Any]] = None
        self._cache_fetched_at: float = 0

    def init(self) -> dict[str, bool | str]:
        """Fetch all flags and cache them. Call once at startup."""
        flags = self._fetch_all_flags()
        if self._cache_ttl_sec > 0:
            self._cache = flags
            self._cache_fetched_at = time.monotonic()
        return flags

    def is_enabled(self, flag_key: str) -> bool:
        """Return whether the flag is enabled (True) or disabled (False)."""
        value = self.get_value(flag_key)
        return value is True

    def get_value(self, flag_key: str) -> bool | str:
        """Return flag value: bool for boolean flags, str for string/enum flags."""
        key = self._normalize_key(flag_key)
        cached = self._get_from_cache()
        if cached is not None and key in cached:
            return cached[key]
        single = self._fetch_single_flag(key)
        if single is not None:
            return single
        return False

    def invalidate_cache(self) -> None:
        """Clear cache so the next is_enabled/get_value refetches."""
        self._cache = None

    def _normalize_key(self, raw: str) -> str:
        return raw.strip().lower().replace(" ", "").replace("_", "-")

    def _get_from_cache(self) -> Optional[dict[str, bool | str]]:
        if self._cache is None or self._cache_ttl_sec <= 0:
            return None
        if time.monotonic() - self._cache_fetched_at > self._cache_ttl_sec:
            self._cache = None
            return None
        return self._cache

    def _request(self, path: str, params: Optional[dict[str, str]] = None) -> dict:
        url = f"{self._base_url}{path}"
        if params:
            q = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
            url = f"{url}?{q}"
        req = urllib.request.Request(
            url,
            headers={
                "X-Api-Key": self._api_key,
                "X-Environment": self._env,
                **({"X-Flag-Email": self._email} if self._email else {}),
            },
        )
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                return json.loads(resp.read().decode())
        except urllib.error.HTTPError as e:
            body = e.read().decode() if e.fp else ""
            raise RuntimeError(
                f"IndieFlags: failed to fetch ({e.code}): {body or e.reason}"
            ) from e

    def _fetch_all_flags(self) -> dict[str, bool | str]:
        params = {"apiKey": self._api_key, "env": self._env}
        if self._email:
            params["email"] = self._email
        data = self._request("/eval/flags", params)
        return data.get("flags") or {}

    def _fetch_single_flag(self, flag_key: str) -> Optional[bool | str]:
        params = {"apiKey": self._api_key, "flag": flag_key, "env": self._env}
        if self._email:
            params["email"] = self._email
        try:
            data = self._request("/eval", params)
        except RuntimeError:
            return None
        if "value" in data and isinstance(data["value"], str):
            return data["value"]
        return data.get("enabled", False)
