"""Abstract LLM provider interface and message types."""

from __future__ import annotations

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


class LLMError(Exception):
    """User-facing LLM error with a clear reason.

    Wraps provider exceptions into messages suitable for showing
    to end-users (in chat, WhatsApp, UI, logs, etc.).
    """

    def __init__(self, reason: str, *, provider: str = "", model: str = "", original: Exception | None = None, retryable: bool = False) -> None:
        self.reason = reason
        self.provider = provider
        self.model = model
        self.original = original
        self.retryable = retryable
        super().__init__(reason)

    @property
    def user_message(self) -> str:
        """Short message safe to show to the user."""
        prefix = f"[{self.model}] " if self.model else ""
        return f"{prefix}{self.reason}"


@dataclass
class Message:
    """A chat message."""

    role: str  # "user" | "assistant" | "tool"
    content: str | list  # str for text, list for tool_use/tool_result blocks
    tool_call_id: str | None = None  # for role="tool" responses


@dataclass
class ImageContent:
    """An image for multimodal messages."""

    base64_data: str
    media_type: str = "image/png"  # image/png, image/jpeg, image/webp, image/gif


@dataclass
class ToolCall:
    """A tool call requested by the LLM."""

    id: str
    name: str
    input: dict


@dataclass
class LLMResponse:
    """Complete response from an LLM provider."""

    content: str
    model: str
    input_tokens: int = 0
    output_tokens: int = 0
    cache_read_tokens: int = 0   # tokens served from prompt cache (discounted)
    cache_write_tokens: int = 0  # tokens written into prompt cache (Anthropic: 25% premium)
    provider_cost: float | None = None  # exact cost from provider (LiteLLM), None = use YAML estimate
    stop_reason: str | None = None
    tool_calls: list[ToolCall] = field(default_factory=list)
    raw_content: Any = None  # provider-specific content blocks


@dataclass
class StreamChunk:
    """An incremental streaming chunk from an LLM provider."""

    delta: str
    done: bool = False
    response: LLMResponse | None = None
    tool_name: str | None = None
    tool_status: str | None = None  # "running", "done", "error"


class LLMProvider(ABC):
    """Abstract base for LLM providers (Anthropic, OpenAI, Ollama)."""

    @abstractmethod
    async def complete(
        self,
        messages: list[Message],
        system: str | None = None,
        max_tokens: int = 4096,
        temperature: float = 0.7,
        model: str | None = None,
    ) -> LLMResponse:
        """Generate a complete response. model overrides the default."""

    @abstractmethod
    def stream(
        self,
        messages: list[Message],
        system: str | None = None,
        max_tokens: int = 4096,
        temperature: float = 0.7,
        model: str | None = None,
    ) -> AsyncIterator[StreamChunk]:
        """Stream response chunks. model overrides the default."""

    async def complete_vision(
        self,
        prompt: str,
        images: list[ImageContent],
        system: str | None = None,
        max_tokens: int = 2048,
        temperature: float = 0.3,
    ) -> LLMResponse:
        """Analyze images with text prompt. Default: builds multimodal message."""
        # Build multimodal content array
        content: list[dict] = [{"type": "text", "text": prompt}]
        for img in images:
            content.append({
                "type": "image_url",
                "image_url": {"url": f"data:{img.media_type};base64,{img.base64_data}"},
            })
        messages = [Message(role="user", content=content)]
        return await self.complete(messages=messages, system=system, max_tokens=max_tokens, temperature=temperature)

    async def complete_with_tools(
        self,
        messages: list[Message],
        tools: list[dict],
        system: str | None = None,
        max_tokens: int = 4096,
        temperature: float = 0.7,
        model: str | None = None,
    ) -> LLMResponse:
        """Generate a response with tool use support.

        Returns LLMResponse with tool_calls populated if the LLM
        wants to use tools. The caller should execute the tools and
        call again with results.

        Default: falls back to complete() without tools.
        Providers override this with native tool support.
        """
        return await self.complete(messages, system, max_tokens, temperature, model=model)
