"""OpenAI-compatible backend implementation.

Works with any OpenAI-compatible API:
- Ollama (http://localhost:11434/v1)
- vLLM (http://localhost:8000/v1)
- llama.cpp (http://localhost:8080/v1)
- llamafile
- OpenAI API itself

Requires: openai package (pip install openai)
"""

import asyncio
import os
from collections.abc import AsyncIterator
from pathlib import Path
from typing import Any

from cleave.core.retry import retry_on_network_errors
from cleave.tui.backends.base import (
    Backend,
    BackendAuthError,
    BackendConfig,
    BackendConnectionError,
    BackendQueryError,
    BackendState,
    Message,
)

# Defer openai import - it's optional
_OPENAI_AVAILABLE: bool | None = None


def _check_openai() -> bool:
    """Check if openai package is available."""
    global _OPENAI_AVAILABLE
    if _OPENAI_AVAILABLE is None:
        try:
            import openai  # noqa: F401

            _OPENAI_AVAILABLE = True
        except ImportError:
            _OPENAI_AVAILABLE = False
    return _OPENAI_AVAILABLE


# Default endpoints for common providers
PROVIDER_ENDPOINTS = {
    "ollama": "http://localhost:11434/v1",
    "vllm": "http://localhost:8000/v1",
    "llamacpp": "http://localhost:8080/v1",
    "openai": "https://api.openai.com/v1",
}

# Recommended models by provider
PROVIDER_MODELS = {
    "ollama": [
        "qwen2.5-coder:7b",
        "qwen3:14b",
        "deepseek-coder-v2:16b",
        "llama3.1:8b",
        "codellama:13b",
    ],
    "vllm": [
        "Qwen/Qwen2.5-Coder-7B-Instruct",
        "Qwen/Qwen3-14B",
    ],
    "llamacpp": [
        "qwen2.5-coder",
    ],
    "openai": [
        "gpt-4o",
        "gpt-4o-mini",
        "gpt-4-turbo",
        "gpt-3.5-turbo",
    ],
}


class OpenAICompatBackend(Backend):
    """Backend for OpenAI-compatible APIs.

    Works with Ollama, vLLM, llama.cpp, llamafile, and OpenAI itself.

    Configuration:
        - base_url: API endpoint (e.g., http://localhost:11434/v1)
        - api_key: API key (required for OpenAI, optional for local)
        - model: Model name
    """

    def __init__(self, provider: str = "ollama") -> None:
        """Initialize OpenAI-compatible backend.

        Args:
            provider: Provider name for defaults (ollama, vllm, llamacpp, openai).
        """
        self._provider = provider
        self._state = BackendState.DISCONNECTED
        self._error: str | None = None
        self._client: Any = None
        self._config: BackendConfig | None = None
        self._conversation: list[dict[str, str]] = []

    @property
    def name(self) -> str:
        """Human-readable backend name."""
        return f"OpenAI ({self._provider})"

    @property
    def state(self) -> BackendState:
        """Current connection state."""
        return self._state

    @property
    def error(self) -> str | None:
        """Last error message."""
        return self._error

    async def connect(self, config: BackendConfig) -> None:
        """Connect to OpenAI-compatible API.

        Args:
            config: Backend configuration.

        Raises:
            BackendConnectionError: If connection fails.
            BackendAuthError: If authentication fails.
        """
        if self._state == BackendState.CONNECTED:
            return

        if not _check_openai():
            self._state = BackendState.ERROR
            self._error = "OpenAI package not installed"
            raise BackendConnectionError(
                "OpenAI package not installed.\n\n"
                "Install with: pip install styrene-cleave[local]\n"
                "Or: pip install openai"
            )

        self._state = BackendState.CONNECTING
        self._error = None
        self._config = config

        try:
            from openai import AsyncOpenAI

            # Determine base URL
            base_url = config.base_url or PROVIDER_ENDPOINTS.get(
                self._provider, PROVIDER_ENDPOINTS["ollama"]
            )

            # Determine API key
            api_key = config.api_key
            if not api_key:
                # Try environment variable
                if self._provider == "openai":
                    api_key = os.environ.get("OPENAI_API_KEY")
                    if not api_key:
                        self._state = BackendState.ERROR
                        self._error = "No API key"
                        raise BackendAuthError(
                            "OpenAI API key not found.\n\n"
                            "Set environment variable: export OPENAI_API_KEY=sk-...\n"
                            "Or provide api_key in config."
                        )
                else:
                    # Local providers don't need a real key
                    api_key = "not-needed"

            self._client = AsyncOpenAI(
                base_url=base_url,
                api_key=api_key,
                timeout=config.timeout,
            )

            # Reset conversation
            self._conversation = []
            if config.system_prompt:
                self._conversation.append({
                    "role": "system",
                    "content": config.system_prompt,
                })

            # Test connection by listing models (optional, may fail on some providers)
            try:
                await self._client.models.list()
            except Exception as e:
                # If it's a local provider, check if server is running
                if self._provider in ("ollama", "vllm", "llamacpp"):
                    error_msg = str(e)
                    if "connection" in error_msg.lower() or "refused" in error_msg.lower():
                        self._state = BackendState.ERROR
                        self._error = f"{self._provider.title()} not running"
                        raise BackendConnectionError(
                            f"{self._provider.title()} server not running.\n\n"
                            f"Start {self._provider} at: {base_url}\n"
                            f"Check the {self._provider} documentation for installation."
                        ) from e

            self._state = BackendState.CONNECTED

        except (BackendAuthError, BackendConnectionError):
            # Re-raise auth/connection errors as-is
            raise
        except Exception as e:
            self._state = BackendState.ERROR
            error_msg = str(e)
            self._error = error_msg

            # Provide actionable error messages
            if "api key" in error_msg.lower() or "auth" in error_msg.lower():
                raise BackendAuthError(
                    f"Authentication failed: {error_msg}\n\n"
                    "Check your API key configuration."
                ) from e
            elif "connection" in error_msg.lower() or "refused" in error_msg.lower():
                provider_name = self._provider.title()
                raise BackendConnectionError(
                    f"Cannot connect to {provider_name} server: {error_msg}\n\n"
                    f"Ensure {provider_name} is running at: {config.base_url or PROVIDER_ENDPOINTS.get(self._provider, 'configured URL')}"
                ) from e
            else:
                raise BackendConnectionError(
                    f"Failed to connect: {error_msg}\n\n"
                    "Check server status and configuration."
                ) from e

    async def disconnect(self) -> None:
        """Disconnect from API."""
        if self._client is not None:
            try:
                await self._client.close()
            except Exception:
                pass
            finally:
                self._client = None
                self._state = BackendState.DISCONNECTED
                self._conversation = []

    async def query(self, prompt: str) -> AsyncIterator[Message]:
        """Send a prompt and stream responses.

        Args:
            prompt: User message to send.

        Yields:
            Message objects as they arrive.

        Raises:
            RuntimeError: If not connected.
            BackendQueryError: If query fails.
        """
        if self._state != BackendState.CONNECTED or self._client is None:
            self._state = BackendState.ERROR
            self._error = "Not connected"
            raise RuntimeError(
                "Backend not connected.\n\n"
                "Call connect() first or check connection status."
            )

        if self._config is None:
            raise RuntimeError("Backend configuration missing")

        # Add user message to conversation
        self._conversation.append({
            "role": "user",
            "content": prompt,
        })

        try:
            model = self._config.model or PROVIDER_MODELS.get(
                self._provider, ["gpt-4o"]
            )[0]

            # Stream response
            stream = await self._client.chat.completions.create(
                model=model,
                messages=self._conversation,
                stream=True,
            )

            full_response = []
            async for chunk in stream:
                if chunk.choices and chunk.choices[0].delta.content:
                    content = chunk.choices[0].delta.content
                    full_response.append(content)
                    yield Message(role="assistant", content=content)

            # Add assistant response to conversation history
            if full_response:
                self._conversation.append({
                    "role": "assistant",
                    "content": "".join(full_response),
                })

        except asyncio.TimeoutError as e:
            self._state = BackendState.ERROR
            self._error = "Request timeout"
            raise BackendQueryError(
                f"Request timed out after {self._config.timeout}s.\n\n"
                "The model is taking too long to respond. Try again or check server status."
            ) from e
        except ConnectionError as e:
            self._state = BackendState.ERROR
            self._error = "Connection lost"
            raise BackendQueryError(
                f"Connection lost during query: {e}\n\n"
                f"Check that {self._provider} server is still running."
            ) from e
        except Exception as e:
            self._state = BackendState.ERROR
            error_msg = str(e)
            self._error = error_msg

            # Provide context-specific error messages
            if "model" in error_msg.lower() and "not found" in error_msg.lower():
                raise BackendQueryError(
                    f"Model not found: {error_msg}\n\n"
                    f"Available models for {self._provider}: {', '.join(PROVIDER_MODELS.get(self._provider, []))}\n"
                    "Check model name in configuration."
                ) from e
            elif "rate limit" in error_msg.lower():
                raise BackendQueryError(
                    f"Rate limit exceeded: {error_msg}\n\n"
                    "Wait before trying again."
                ) from e
            elif "context" in error_msg.lower() or "token" in error_msg.lower():
                raise BackendQueryError(
                    f"Context limit exceeded: {error_msg}\n\n"
                    "Try a shorter conversation or start a new session."
                ) from e
            elif "connection" in error_msg.lower():
                raise BackendQueryError(
                    f"Connection error: {error_msg}\n\n"
                    f"Check that {self._provider} server is running."
                ) from e
            else:
                raise BackendQueryError(
                    f"Query failed: {error_msg}\n\n"
                    "Check error details and try again."
                ) from e

    @classmethod
    def check_auth(cls, provider: str | None = None, base_url: str | None = None) -> tuple[bool, str]:
        """Check if backend is available.

        For local providers (Ollama, vLLM, llama.cpp), tests endpoint connectivity.
        For OpenAI, validates API key format and optionally tests connectivity.

        Args:
            provider: Provider name (ollama, openai, vllm, llamacpp). If None, checks
                     OpenAI API key first, then Ollama.
            base_url: Optional base URL to test (overrides provider default).

        Returns:
            Tuple of (is_available, status_message).
        """
        if not _check_openai():
            return False, "OpenAI package not installed"

        # If no provider specified, check OpenAI key first, then Ollama (legacy behavior)
        if provider is None:
            # Check for OpenAI API key
            api_key = os.environ.get("OPENAI_API_KEY")
            if api_key:
                if len(api_key) >= 20 and api_key.startswith("sk-"):
                    return True, "OpenAI API key configured"
                else:
                    return False, "OPENAI_API_KEY appears to be invalid"

            # Check for local Ollama
            try:
                import socket
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.settimeout(1)
                result = sock.connect_ex(("localhost", 11434))
                sock.close()
                if result == 0:
                    return True, "Ollama running on localhost:11434"
            except Exception:
                pass

            return False, "No OpenAI API key and Ollama not running"

        # Determine endpoint to test
        endpoint = base_url or PROVIDER_ENDPOINTS.get(provider, PROVIDER_ENDPOINTS["ollama"])

        # OpenAI provider - validate API key
        if provider == "openai":
            api_key = os.environ.get("OPENAI_API_KEY")
            if not api_key:
                return False, "OPENAI_API_KEY environment variable not set"
            if not isinstance(api_key, str) or len(api_key) < 20:
                return False, "OPENAI_API_KEY appears to be invalid (too short)"
            if not api_key.startswith("sk-"):
                return False, "OPENAI_API_KEY has invalid format (should start with 'sk-')"
            # Note: We don't test actual API connectivity to avoid rate limits
            return True, "OpenAI API key configured"

        # Local providers - test endpoint connectivity
        try:
            from urllib.parse import urlparse
            import socket

            parsed = urlparse(endpoint)
            host = parsed.hostname or "localhost"
            port = parsed.port

            if port is None:
                # Infer port from scheme
                port = 443 if parsed.scheme == "https" else 80

            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(2)
            result = sock.connect_ex((host, port))
            sock.close()

            if result == 0:
                return True, f"{provider.capitalize()} server reachable at {endpoint}"
            else:
                return False, f"{provider.capitalize()} server not reachable at {endpoint}"

        except Exception as e:
            return False, f"Error checking {provider} connectivity: {e}"

    @classmethod
    def available_models(cls, config: BackendConfig | None = None) -> list[str]:
        """List available models.

        Args:
            config: Config with base_url for dynamic discovery.

        Returns:
            List of model names.
        """
        # If config provided with base_url, could do dynamic discovery
        # For now, return static list based on provider
        # TODO: Add dynamic model discovery via API

        # Return combined list of common models
        models = []
        for provider_models in PROVIDER_MODELS.values():
            for m in provider_models:
                if m not in models:
                    models.append(m)
        return models

    @classmethod
    def default_config(cls) -> BackendConfig:
        """Get default configuration.

        Returns:
            BackendConfig with Ollama defaults.
        """
        return BackendConfig(
            base_url=PROVIDER_ENDPOINTS["ollama"],
            model="qwen2.5-coder:7b",
            timeout=120.0,
        )

    def reset_conversation(self) -> None:
        """Clear conversation history.

        Useful for starting a fresh context.
        """
        self._conversation = []
        if self._config and self._config.system_prompt:
            self._conversation.append({
                "role": "system",
                "content": self._config.system_prompt,
            })
