"""Base provider with secure API key loading and thread-safe client init.

Key resolution order:
  1. keyring (macOS Keychain / Windows Credential Manager)
  2. Environment variable
  3. None (provider disabled)

API keys are NEVER read from plaintext config files.
"""

from __future__ import annotations

import logging
import os
import threading
from collections.abc import Generator
from typing import Any

from .base import AIRequest, AIResponse

log = logging.getLogger(__name__)

_SERVICE = "MuraveyChina"

# Pattern to strip API keys and URL query parameters from error messages.
_SENSITIVE_RE = __import__("re").compile(
    r"((?:key|token|secret|password|api.?key)[=:\s]\s*)(\S+)"
    r"|(\?[^\s'\")\]]+)",
    __import__("re").IGNORECASE,
)


def _sanitize_error(exc: Exception) -> str:
    """Return a sanitized error string with API keys and query params removed."""
    msg = str(exc)
    msg = _SENSITIVE_RE.sub(
        lambda m: (m.group(1) + "***") if m.group(1) else "?<redacted>",
        msg,
    )
    return msg


class BaseAPIProvider:
    """Loads API key securely and manages lazy client initialisation.

    Also provides OpenAI-compatible ask()/stream()/_build_messages()
    shared by all providers (DeepSeek, Aliyun, 88API).
    """

    # Subclasses must set these
    name: str = "base"
    ENV_VAR: str = ""
    DEFAULT_MODEL: str = ""

    def __init__(self, proxy_url: str = "") -> None:
        self.api_key: str = self._resolve_key()
        self._proxy_url: str = proxy_url
        self._client: Any | None = None
        self._client_lock = threading.Lock()

        if self.api_key:
            log.debug("[%s] API key loaded", self.name)
        else:
            log.warning("[%s] No API key found — provider will be unavailable", self.name)
        if proxy_url:
            log.debug("[%s] Using proxy: %s", self.name, proxy_url)

    # ------------------------------------------------------------------ #
    # Key resolution
    # ------------------------------------------------------------------ #

    def _resolve_key(self) -> str:
        """Try keyring → ENV → empty string."""
        # Priority 1: keyring (Keychain / Credential Manager)
        try:
            import keyring

            stored = keyring.get_password(_SERVICE, f"{self.name}_api_key")
            if stored:
                return stored
        except Exception as exc:
            log.debug("[%s] keyring unavailable: %s", self.name, exc)

        # Priority 2: Environment variable
        if self.ENV_VAR:
            env_key = os.environ.get(self.ENV_VAR, "")
            if env_key:
                return env_key

        return ""

    # ------------------------------------------------------------------ #
    # Thread-safe lazy client
    # ------------------------------------------------------------------ #

    def _get_client(self) -> Any:
        """Double-checked locking for thread-safe lazy initialisation."""
        if self._client is None:
            with self._client_lock:
                if self._client is None:
                    self._client = self._create_client()
        return self._client

    def _make_http_client(self, timeout: float = 90.0) -> Any:
        """Create an httpx.Client with proxy and keepalive support.

        Returns an httpx.Client if a proxy is set, or None (let openai use its default).
        Uses aggressive keepalive to survive CSCA network blocking.
        """
        if not self._proxy_url:
            return None
        try:
            import httpx
            import httpcore

            # Aggressive keepalive: 60s expiry keeps connections alive through CSCA
            transport = httpcore.ConnectionPool(
                proxy=httpcore.Proxy(url=self._proxy_url) if self._proxy_url else None,
                keepalive_expiry=60.0,
                retries=2,
            )
            return httpx.Client(
                proxy=self._proxy_url,
                timeout=httpx.Timeout(timeout, connect=15.0),
                transport=transport,
            )
        except ImportError:
            log.warning("[%s] httpx not installed — proxy will not be used", self.name)
            return None
        except Exception as exc:
            log.warning("[%s] Failed to create proxy http_client: %s", self.name, exc)
            # Fallback: try without custom transport
            try:
                import httpx
                return httpx.Client(
                    proxy=self._proxy_url,
                    timeout=httpx.Timeout(timeout, connect=15.0),
                )
            except Exception:
                return None

    def _create_client(self) -> Any:
        """Subclasses must override to return the provider SDK client."""
        raise NotImplementedError

    def is_available(self) -> bool:
        """Return True only if an API key is present."""
        return bool(self.api_key)

    # ------------------------------------------------------------------ #
    # OpenAI-compatible request helpers (shared by all providers)
    # ------------------------------------------------------------------ #

    def _build_messages(self, request: AIRequest) -> list[dict[str, str]]:
        if request.messages:
            return request.messages
        messages: list[dict[str, str]] = []
        if request.system_prompt:
            messages.append({"role": "system", "content": request.system_prompt})
        messages.append({"role": "user", "content": request.prompt})
        return messages

    def ask(self, request: AIRequest) -> AIResponse:
        """Send a request and return a response.

        Exceptions are intentionally NOT caught here — they propagate to
        ``retry_with_backoff`` (transient errors are retried) and then to
        ``ChinaAdapter.ask()`` which handles cross-provider failover.
        """
        client = self._get_client()
        model = request.model or self.DEFAULT_MODEL
        response = client.chat.completions.create(
            model=model,
            messages=self._build_messages(request),
            temperature=request.temperature,
            max_tokens=request.max_tokens,
        )
        text = response.choices[0].message.content or ""
        tokens = response.usage.total_tokens if response.usage else 0
        return AIResponse(text=text.strip(), provider=self.name, model=model, tokens_used=tokens)

    def stream(self, request: AIRequest) -> Generator[str, None, None]:
        client = self._get_client()
        model = request.model or self.DEFAULT_MODEL
        try:
            response = client.chat.completions.create(
                model=model,
                messages=self._build_messages(request),
                temperature=request.temperature,
                max_tokens=request.max_tokens,
                stream=True,
            )
            for chunk in response:
                if chunk.choices and chunk.choices[0].delta.content:
                    yield chunk.choices[0].delta.content
        except Exception as exc:
            log.error("[%s] stream() error: %s", self.name, exc)
            yield f"\n[Error: {_sanitize_error(exc)}]"
