"""Abstract backend interface for LLM inference.

This module defines the provider-agnostic interface that different
backends (Claude SDK, OpenAI-compatible, etc.) must implement.
"""

from abc import ABC, abstractmethod
from collections.abc import AsyncIterator
from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class BackendState(Enum):
    """Backend connection state."""

    DISCONNECTED = "disconnected"
    CONNECTING = "connecting"
    CONNECTED = "connected"
    ERROR = "error"


class MessageType(Enum):
    """Type of message for streaming support."""

    COMPLETE = "complete"  # Full message (non-streaming)
    STREAM_START = "stream_start"  # Begin streaming a message
    STREAM_DELTA = "stream_delta"  # Token/chunk during streaming
    STREAM_END = "stream_end"  # End of streaming message


@dataclass
class Message:
    """A message from the backend.

    This is the common message format that all backends produce,
    regardless of their underlying API.

    For streaming support, messages can have different types:
    - COMPLETE: A full message (legacy/non-streaming mode)
    - STREAM_START: Signals start of a streaming message
    - STREAM_DELTA: A token/chunk during streaming
    - STREAM_END: Signals end of streaming, may contain full content
    """

    role: str  # "user", "assistant", "system", "tool"
    content: str
    metadata: dict[str, Any] = field(default_factory=dict)
    message_type: MessageType = MessageType.COMPLETE

    @property
    def is_tool_use(self) -> bool:
        """Whether this message represents a tool invocation."""
        return self.role == "tool" or "tool" in self.metadata

    @property
    def is_error(self) -> bool:
        """Whether this message represents an error."""
        return self.metadata.get("error", False)

    @property
    def is_streaming(self) -> bool:
        """Whether this is a streaming message (start, delta, or end)."""
        return self.message_type in (
            MessageType.STREAM_START,
            MessageType.STREAM_DELTA,
            MessageType.STREAM_END,
        )


@dataclass
class BackendConfig:
    """Configuration for a backend connection.

    Backends may use different subsets of these fields.
    """

    # Model selection
    model: str = ""

    # Connection settings
    base_url: str | None = None
    api_key: str | None = None
    timeout: float = 120.0

    # Session settings
    max_turns: int = 50
    system_prompt: str | None = None

    # Tool configuration
    allowed_tools: list[str] | None = None
    disallowed_tools: list[str] | None = None

    # Working context
    working_directory: str | None = None


class Backend(ABC):
    """Abstract backend for LLM inference.

    All LLM providers (Claude SDK, OpenAI, Ollama, etc.) implement
    this interface to provide a consistent API for the TUI.

    Lifecycle:
        1. Create backend instance
        2. Call connect() to establish connection
        3. Call query() to send prompts and receive responses
        4. Call disconnect() when done

    Example:
        backend = ClaudeBackend()
        await backend.connect(config)
        async for msg in backend.query("Hello"):
            print(msg.content)
        await backend.disconnect()
    """

    @property
    @abstractmethod
    def name(self) -> str:
        """Human-readable backend name (e.g., 'Claude', 'Ollama')."""
        ...

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

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

    @abstractmethod
    async def connect(self, config: BackendConfig) -> None:
        """Establish connection to the backend.

        Args:
            config: Backend configuration.

        Raises:
            ConnectionError: If connection fails.
        """
        ...

    @abstractmethod
    async def disconnect(self) -> None:
        """Close the backend connection.

        Safe to call multiple times or if not connected.
        """
        ...

    @abstractmethod
    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.
            ConnectionError: If connection is lost during query.
        """
        ...

    @classmethod
    @abstractmethod
    def check_auth(cls) -> tuple[bool, str]:
        """Check if authentication is configured.

        Returns:
            Tuple of (is_authenticated, status_message).
        """
        ...

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

        Args:
            config: Optional config for dynamic model discovery.

        Returns:
            List of model identifiers.
        """
        ...

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

        Returns:
            BackendConfig with sensible defaults.
        """
        return BackendConfig()


class BackendError(Exception):
    """Base exception for backend errors."""

    pass


class BackendConnectionError(BackendError):
    """Raised when connection to backend fails."""

    pass


class BackendAuthError(BackendError):
    """Raised when authentication fails or is missing."""

    pass


class BackendQueryError(BackendError):
    """Raised when a query fails."""

    pass
