"""AI provider abstraction — minimal P2 surface.

Per the approved plan (§Provider abstraction + model routing): providers
implement a small Protocol so the chat tier can swap Gemini for Claude with
a one-line config change later. P2 needs exactly two capabilities:

- ``chat_stream`` — ONE streaming model call. Yields StreamEvents.
- ``complete``   — one-shot non-streaming text call (session titling).

The StreamEvent ``type`` vocabulary is the PINNED SSE contract
(``text_delta | tool_call | tool_result | message_done | error``;
``resolution_card`` joins in P4). Providers themselves emit only
``text_delta`` / ``tool_call`` / ``message_done`` / ``error`` — the
FUNCTION-CALLING LOOP BELONGS TO THE CALLER (services/ai/chat.py), which
dispatches tools and emits ``tool_result`` events between provider calls.
A provider never dispatches tools and never touches the DB.

Message/Part/ToolSpec are provider-neutral: role vocabulary is
``user | assistant | tool`` (each provider maps to its own wire roles), and
``ToolSpec.parameters`` is a plain JSON-schema dict (object/properties/
required subset — see services/ai/registry.py, which generates it from
pydantic models).
"""
from __future__ import annotations

from typing import AsyncIterator, Literal, Protocol

from pydantic import BaseModel, Field


class InlineData(BaseModel):
    """Base64-encoded media attached to a message part (P2: chat photos
    travel inline, v1 parity; the Files API upgrade is post-cutover)."""

    mime_type: str
    data: str  # base64


class FunctionCall(BaseModel):
    name: str
    args: dict = Field(default_factory=dict)


class FunctionResponse(BaseModel):
    name: str
    response: dict = Field(default_factory=dict)


class Part(BaseModel):
    """Exactly one of the fields is set (loose union, v1's Gemini part
    shape made provider-neutral)."""

    text: str | None = None
    inline_data: InlineData | None = None
    function_call: FunctionCall | None = None
    function_response: FunctionResponse | None = None


class Message(BaseModel):
    role: Literal["user", "assistant", "tool"]
    parts: list[Part] = Field(default_factory=list)


class ToolSpec(BaseModel):
    """One callable tool, in declaration form. ``parameters`` is a JSON
    schema dict (``{"type": "object", "properties": {...}, "required": [...]}``)."""

    name: str
    description: str = ""
    parameters: dict = Field(
        default_factory=lambda: {"type": "object", "properties": {}}
    )


StreamEventType = Literal[
    "text_delta", "tool_call", "tool_result", "message_done", "error"
]


class StreamEvent(BaseModel):
    """One SSE-bound event.

    data payloads by type:
      text_delta   {"text": str}
      tool_call    {"name": str, "args": dict}
      tool_result  {"name": str, "args": dict, "result": dict}   (orchestrator)
      message_done {"text": str, "tool_calls": list}
      error        {"code": str, "message": str}
    """

    type: StreamEventType
    data: dict = Field(default_factory=dict)


class AIProvider(Protocol):
    """Minimal P2 provider contract (chat + titling)."""

    def chat_stream(
        self,
        *,
        model: str,
        system: str,
        messages: list[Message],
        tools: list[ToolSpec],
    ) -> AsyncIterator[StreamEvent]:
        """One streaming model call. Yields text_delta / tool_call events as
        they arrive and ALWAYS finishes with either a message_done (data:
        full text + collected tool calls) or a terminal error event. Never
        raises mid-stream — failures become error events."""
        ...

    async def complete(
        self, *, model: str, prompt: str, temperature: float = 0.0
    ) -> str:
        """One-shot non-streaming text completion (used for session
        titling). Raises AppError on failure."""
        ...
