from typing import Set, Callable, Optional

import threading

from dataclasses import dataclass
import atexit

from frisk_sdk.core.access_token_provider import AccessTokenProvider
from frisk_sdk.errors.configuration_errors import TokenRuntimeConfigurationConflictError


@dataclass(frozen=True)
class _TokenRuntimeConfig:
    api_key: str
    base_url: str
    refresh_interval_minutes: int

    def redacted(self) -> str:
        if not self.api_key:
            ak = "<empty>"
        elif len(self.api_key) <= 6:
            ak = self.api_key[:2] + "…"
        else:
            ak = self.api_key[:3] + "…" + self.api_key[-2:]
        return f"_TokenRuntimeConfig(api_key={ak}, api_base_url={self.base_url!r}, refresh={self.refresh_interval_minutes}m)"


class AccessTokenProviderRuntime:
    """A process-wide shared token refresher with subscription support.

    - Owns exactly one AccessTokenProvider + background refresh thread for a given config.
    - Allows many Frisk() instances to subscribe to token updates without spawning
      extra threads.
    """

    def __init__(
        self, cfg: _TokenRuntimeConfig, *, parent_logger_name: Optional[str] = None
    ) -> None:
        self.cfg = cfg
        self._lock = threading.Lock()
        self._subscribers: Set[Callable[[str], None]] = set()

        # A single dispatcher callback is registered with AccessTokenProvider.
        def _dispatch(token: str) -> None:
            with self._lock:
                subs = list(self._subscribers)
            for cb in subs:
                try:
                    cb(token)
                except Exception:
                    # Never let a subscriber break the refresh loop.
                    pass

        self._tm = AccessTokenProvider(
            api_key=cfg.api_key,
            refresh_interval_minutes=cfg.refresh_interval_minutes,
            base_url=cfg.base_url,
            callbacks=[_dispatch],
            parent_logger_name=parent_logger_name,
        )
        self._tm.start_background_refresh()

    def get_access_token(self) -> str:
        return self._tm.get_access_token()

    def subscribe(self, cb: Callable[[str], None]) -> None:
        with self._lock:
            self._subscribers.add(cb)

    def unsubscribe(self, cb: Callable[[str], None]) -> None:
        with self._lock:
            self._subscribers.discard(cb)

    def stop(self) -> None:
        self._tm.stop_background_refresh()


# Global singleton runtime (C1 pattern)
_runtime_lock = threading.Lock()
_runtime: Optional[AccessTokenProviderRuntime] = None


def get_or_init_access_token_provider_runtime(
    *,
    api_key: str,
    base_url: str,
    refresh_interval_minutes: int,
    parent_logger_name: Optional[str] = None,
) -> AccessTokenProviderRuntime:
    """Get the shared AccessTokenProviderRuntime, creating it if needed.

    If called again with conflicting (api_key/api_base_url/refresh_interval), raises.
    """
    global _runtime

    cfg = _TokenRuntimeConfig(
        api_key=api_key,
        base_url=base_url,
        refresh_interval_minutes=refresh_interval_minutes,
    )

    with _runtime_lock:
        if _runtime is None:
            _runtime = AccessTokenProviderRuntime(
                cfg, parent_logger_name=parent_logger_name
            )
            return _runtime

        if _runtime.cfg != cfg:
            raise TokenRuntimeConfigurationConflictError()

        return _runtime


def reset_token_runtime_for_testing() -> None:
    """Stop and clear the shared runtime. Intended for tests."""
    global _runtime
    with _runtime_lock:
        if _runtime is not None:
            _runtime.stop()
        _runtime = None


def _shutdown_runtime_atexit() -> None:
    try:
        reset_token_runtime_for_testing()
    except Exception:
        pass


atexit.register(_shutdown_runtime_atexit)
