"""Factory and router for Chinese AI providers.

Usage:
    adapter = ChinaAdapter(config)
    provider = adapter.get_provider()                       # get configured provider
    response = adapter.ask(request)                        # ask with retry
    response = adapter.ask(request, stream=True)           # delegates to stream()
    for chunk in adapter.stream(request): ...              # streaming, no retry
    for chunk in adapter.stream(request, cancel): ...      # streaming with cancel
"""

from __future__ import annotations

import dataclasses
import logging
import threading
from collections.abc import Generator

from .base import AIProvider, AIRequest, AIResponse
from .retry import retry_with_backoff

log = logging.getLogger(__name__)

PROVIDER_NAMES = ("deepseek", "aliyun", "88api", "ollama", "proxy")


def _resolve_proxy_url(config: object) -> str:
    """Resolve proxy URL from config + auto-detection + NetworkProtector.

    Returns proxy URL string or empty string if no proxy needed/found.
    """
    manual = getattr(config, "proxy_url", "")
    if manual:
        return manual

    # Use NetworkProtector proxy only if verified alive
    try:
        from ..utils.network_protect import _active_protector
        if (_active_protector and _active_protector.proxy_url
                and getattr(_active_protector, "_proxy_alive", False)):
            return _active_protector.proxy_url
    except ImportError:
        pass

    auto_detect = getattr(config, "proxy_auto_detect", True)
    if not auto_detect:
        return ""

    try:
        from ..utils.gfw_detect import get_proxy_if_needed
        return get_proxy_if_needed()
    except Exception as exc:
        log.debug("Proxy auto-detection failed: %s", exc)
        return ""


def _load_provider(name: str, config: object = None, proxy_url: str = "") -> AIProvider:
    """Lazy import to avoid importing platform-specific deps at startup."""
    if name == "deepseek":
        from .provider_deepseek import DeepseekProvider

        return DeepseekProvider(proxy_url=proxy_url)
    elif name == "aliyun":
        from .provider_aliyun import AliyunProvider

        return AliyunProvider(proxy_url=proxy_url)
    elif name == "88api":
        from .provider_88api import API88Provider

        return API88Provider(proxy_url=proxy_url)
    elif name == "ollama":
        from .provider_ollama import DEFAULT_BASE_URL, OllamaProvider

        base_url = (
            getattr(config, "ollama_base_url", DEFAULT_BASE_URL) if config else DEFAULT_BASE_URL
        )
        return OllamaProvider(base_url=base_url)
    elif name == "proxy":
        from .provider_proxy import PROXY_BASE_URL, ProxyProvider
        from ..license.license_manager_v2 import load_session

        session = load_session()
        session_key = session.get("session_key", "") if session else ""
        base_url = getattr(config, "proxy_base_url", PROXY_BASE_URL) if config else PROXY_BASE_URL
        return ProxyProvider(session_key=session_key, base_url=base_url, proxy_url=proxy_url)
    else:
        raise ValueError(f"Unknown provider: {name!r}. Choose from: {PROVIDER_NAMES}")


class ChinaAdapter:
    """Routes AI requests to the configured Chinese provider with retry and failover."""

    def __init__(self, config: object) -> None:
        """Accept Config or any object with .provider and .model attributes."""
        self._config = config
        self._provider: AIProvider | None = None
        self._providers_cache: dict[str, AIProvider] = {}
        self._proxy_url: str | None = None  # lazy-resolved
        self._ssh_tunnel = None

    @property
    def provider_name(self) -> str:
        return getattr(self._config, "provider", "deepseek")

    def _get_or_create(self, name: str) -> AIProvider:
        """Get cached provider or create a new one."""
        current_proxy = self.get_proxy_url()

        # Invalidate cached provider if proxy URL changed (e.g. NetworkProtector started)
        if name in self._providers_cache:
            cached = self._providers_cache[name]
            cached_proxy = getattr(cached, "_proxy_url", "")
            if cached_proxy != current_proxy and current_proxy:
                log.info("[adapter] Proxy changed for %s: %s → %s — recreating",
                         name, cached_proxy or "(none)", current_proxy)
                del self._providers_cache[name]

        if name not in self._providers_cache:
            self._providers_cache[name] = _load_provider(
                name, self._config, proxy_url=current_proxy,
            )
        return self._providers_cache[name]

    def get_provider(self) -> AIProvider:
        """Return (cached) primary provider instance."""
        if self._provider is None:
            self._provider = self._get_or_create(self.provider_name)
        return self._provider

    def _iter_available(self) -> list[AIProvider]:
        """Return all providers with API keys, in fallback-chain order."""
        chain = list(getattr(self._config, "fallback_chain", ["deepseek", "aliyun", "88api", "ollama"]))
        primary = self.provider_name

        if primary not in chain:
            chain = [primary] + list(chain)

        available: list[AIProvider] = []
        for name in chain:
            try:
                provider = self._get_or_create(name)
                if provider.is_available():
                    available.append(provider)
            except Exception as e:
                log.debug("Provider '%s' skipped during availability check: %s", name, e)
        return available

    def get_available_provider(self) -> AIProvider:
        """Try providers in fallback order until one is available."""
        providers = self._iter_available()
        if providers:
            return providers[0]
        log.warning("No provider available in fallback chain — using primary as fallback")
        return self.get_provider()

    def reset_provider(self) -> None:
        """Force re-creation of provider (e.g., after key change)."""
        self._provider = None
        self._providers_cache.clear()
        self._proxy_url = None

    # ------------------------------------------------------------------ #
    # Proxy / tunnel
    # ------------------------------------------------------------------ #

    def get_proxy_url(self) -> str:
        """Return resolved proxy URL. Re-checks if previously empty."""
        if self._proxy_url is None or self._proxy_url == "":
            self._proxy_url = _resolve_proxy_url(self._config)
        return self._proxy_url or ""

    def start_ssh_tunnel(self) -> bool:
        """Start SSH tunnel if configured. Returns True on success."""
        cfg = self._config
        if not getattr(cfg, "ssh_tunnel_enabled", False):
            return False
        target = getattr(cfg, "ssh_tunnel_target", "")
        if not target:
            log.warning("SSH tunnel enabled but no target configured")
            return False

        from ..utils.proxy_tunnel import SSHTunnel

        self._ssh_tunnel = SSHTunnel(
            ssh_target=target,
            local_port=getattr(cfg, "ssh_tunnel_local_port", 11080),
            remote_host=getattr(cfg, "ssh_tunnel_remote_host", "127.0.0.1"),
            remote_port=getattr(cfg, "ssh_tunnel_remote_port", 443),
            ssh_port=getattr(cfg, "ssh_tunnel_ssh_port", 22),
            ssh_key=getattr(cfg, "ssh_tunnel_ssh_key", ""),
        )
        ok = self._ssh_tunnel.start()
        if ok:
            # Override proxy to use the tunnel
            self._proxy_url = self._ssh_tunnel.local_url
            # Reset providers so they pick up the new proxy
            self._provider = None
            self._providers_cache.clear()
        return ok

    def stop_ssh_tunnel(self) -> None:
        """Stop SSH tunnel if running."""
        if self._ssh_tunnel:
            self._ssh_tunnel.stop()
            self._ssh_tunnel = None
            self._proxy_url = None

    def ask(
        self,
        request: AIRequest,
        stream: bool = False,
        cancel_event: threading.Event | None = None,
    ) -> AIResponse | Generator[str, None, None]:
        """Send request with exponential backoff retry and runtime failover.

        Tries each available provider in fallback-chain order.  If the primary
        provider returns an error response (or raises), the next available one is
        tried automatically.  When *stream=True*, delegates to :meth:`stream`.
        """
        if stream:
            return self.stream(request, cancel_event=cancel_event)

        # Set model from config if not explicitly specified in the request
        if request.model is None:
            request.model = getattr(self._config, "model", None)

        providers = self._iter_available()
        if not providers:
            provider = self.get_provider()
            return AIResponse(
                text="",
                provider=provider.name,
                model=request.model or "",
                error=f"No API key configured for provider '{provider.name}'.",
            )

        last_error = ""
        for i, provider in enumerate(providers):
            if i > 0:
                # Failover: use this provider's own default model — the primary
                # model name may not be valid for the fallback provider.
                fallback_model = getattr(provider, "DEFAULT_MODEL", request.model)
                req = dataclasses.replace(request, model=fallback_model)
                log.info(
                    "ask() failing over to provider '%s' (model=%s)",
                    provider.name,
                    fallback_model,
                )
            else:
                req = request

            try:
                resp = retry_with_backoff(provider.ask, req, max_retries=1)
            except Exception as exc:
                last_error = str(exc)
                log.warning("ask() provider '%s' raised: %s", provider.name, exc)
                continue

            if resp.ok:
                return resp

            last_error = resp.error or "unknown error"
            log.warning("ask() provider '%s' returned error: %s", provider.name, last_error)

        return AIResponse(
            text="",
            provider=providers[0].name,
            model=request.model or "",
            error=last_error,
        )

    def stream(
        self,
        request: AIRequest,
        cancel_event: threading.Event | None = None,
    ) -> Generator[str, None, None]:
        """Stream response with failover — no retry (can't rewind safely)."""
        provider = self.get_available_provider()
        if not provider.is_available():
            yield f"[Error: No API key configured for '{provider.name}'.]"
            return

        if request.model is None:
            request.model = getattr(self._config, "model", None)

        for chunk in provider.stream(request):
            if cancel_event is not None and cancel_event.is_set():
                log.debug("stream(): cancel_event set — stopping generator")
                return
            yield chunk

    @staticmethod
    def available_providers() -> list[str]:
        """Return list of known provider names."""
        return list(PROVIDER_NAMES)
