"""
LLM abstraction layer.

Provides a unified interface for different LLM providers (OpenAI, Anthropic, local models)
to enable easy switching and testing without changing application code.
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional


@dataclass
class LLMMessage:
    """
    Represents a message in a conversation.

    Attributes:
        role: Message role ("system", "user", "assistant")
        content: Message text content
        metadata: Optional metadata about the message
    """

    role: str  # "system", "user", "assistant"
    content: str
    metadata: Optional[Dict[str, Any]] = None


@dataclass
class LLMResponse:
    """
    Standardized response from an LLM.

    Attributes:
        content: Generated text response
        model: Model identifier used for generation
        usage: Token usage statistics
        finish_reason: Why generation stopped ("stop", "length", "content_filter")
        metadata: Additional provider-specific metadata
    """

    content: str
    model: str
    usage: Dict[str, int] = field(default_factory=dict)
    finish_reason: Optional[str] = None
    metadata: Dict[str, Any] = field(default_factory=dict)


# pylint: disable=unnecessary-pass
class BaseLLM(ABC):
    """
    Abstract base class for LLM providers.

    Provides a consistent interface for generating text using different
    LLM backends (OpenAI, Anthropic, local models, etc.).

    Example:
        llm = OpenAILLM(api_key="sk-...", model="gpt-4")

        messages = [
            LLMMessage(role="system", content="You are a helpful assistant."),
            LLMMessage(role="user", content="Does this video have music?")
        ]

        response = llm.generate(messages, temperature=0.7)
        print(response.content)
    """

    @abstractmethod
    def generate(
        self,
        messages: List[LLMMessage],
        temperature: float = 0.1,
        max_tokens: Optional[int] = None,
        **kwargs,
    ) -> LLMResponse:
        """
        Generate a response from the LLM.

        Args:
            messages: Conversation history as list of LLMMessage objects
            temperature: Sampling temperature (0.0 = deterministic, 1.0 = creative)
            max_tokens: Maximum tokens to generate (None = model default)
            **kwargs: Provider-specific parameters

        Returns:
            LLMResponse containing generated text and metadata

        Raises:
            LLMError: If generation fails
            ValueError: If messages are invalid
        """

    @abstractmethod
    def generate_stream(
        self,
        messages: List[LLMMessage],
        temperature: float = 0.1,
        max_tokens: Optional[int] = None,
        **kwargs,
    ):
        """
        Generate a streaming response from the LLM.

        Yields content chunks as they're generated, enabling real-time display.

        Args:
            messages: Conversation history
            temperature: Sampling temperature
            max_tokens: Maximum tokens to generate
            **kwargs: Provider-specific parameters

        Yields:
            String chunks of generated content

        Raises:
            LLMError: If generation fails
        """
        pass

    @abstractmethod
    def embed(self, text: str) -> List[float]:
        """
        Generate an embedding vector for text.

        Not all LLM providers support embeddings. Override only if supported.

        Args:
            text: Text to embed

        Returns:
            Embedding vector as list of floats

        Raises:
            NotImplementedError: If provider doesn't support embeddings
            LLMError: If embedding generation fails
        """
        raise NotImplementedError(f"{self.__class__.__name__} does not support embeddings")

    @property
    @abstractmethod
    def model_name(self) -> str:
        """
        Identifier for the model being used.

        Returns:
            Model name string (e.g., "gpt-4", "claude-3-opus")
        """
        pass

    @property
    def supports_streaming(self) -> bool:
        """
        Whether this LLM provider supports streaming responses.

        Override to return False if streaming is not available.

        Returns:
            True if streaming is supported
        """
        return True

    @property
    def supports_embeddings(self) -> bool:
        """
        Whether this LLM provider supports text embeddings.

        Override to return True if embeddings are available.

        Returns:
            True if embeddings are supported
        """
        return False

    @property
    def max_context_length(self) -> int:
        """
        Maximum context length (tokens) supported by this model.

        Override with model-specific values.

        Returns:
            Maximum number of tokens in context window
        """
        return 4096  # Conservative default

    def count_tokens(self, text: str) -> int:
        """
        Estimate token count for text.

        Default implementation prefers LiteLLM's tokenizer when available,
        then falls back to a rough approximation (4 chars ≈ 1 token).
        Override with provider-specific tokenizer for accuracy.

        Args:
            text: Text to count tokens for

        Returns:
            Estimated token count
        """
        try:
            from litellm import token_counter  # pylint: disable=import-outside-toplevel

            if getattr(self, "model_name", None):
                return token_counter(model=self.model_name, text=text)
        except Exception:  # pylint: disable=broad-exception-caught
            pass

        # Rough approximation: ~4 characters per token
        return len(text) // 4

    def validate_messages(self, messages: List[LLMMessage]) -> bool:
        """
        Validate message format before sending to LLM.

        Args:
            messages: List of messages to validate

        Returns:
            True if valid

        Raises:
            ValueError: If messages are invalid
        """
        if not messages:
            raise ValueError("Messages list cannot be empty")

        for msg in messages:
            if msg.role not in ["system", "user", "assistant"]:
                raise ValueError(
                    f"Invalid role: {msg.role}. Must be 'system', 'user', or 'assistant'"
                )
            if not msg.content or not msg.content.strip():
                raise ValueError("Message content cannot be empty")

        return True
