from __future__ import annotations

import base64
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any

from ...._endpoints import Characters, Route
from ...._internal._builders.characters import (
    build_character_chat_body,
    build_character_voice_chat_body,
)
from ...._internal._schemas import APIEnvelope
from ...._internal._schemas.characters.characters_chat import Message
from ...._internal._schemas.characters.characters_messages import (
    CharacterChatCompletionChunk,
    CharacterChatCompletionEnd,
    CharacterChatCompletionResult,
    CharacterChatVoiceChunk,
    CharacterChatVoiceEnd,
    HistoryCharacterMessages,
    HistoryCharacterMessagesBody,
    StreamItem,
    VoiceStreamItem,
)
from ...._utils import normalize_history
from ....exceptions import StreamInterruptedError
from ..._base import BaseResource

if TYPE_CHECKING:
    from collections.abc import Iterator
    from types import SimpleNamespace

    from ...._sync_client import SyncClient
    from ...._types import HistoryInput
else:
    SyncClient = Any

logger = logging.getLogger(__name__)


class CharactersMessagesResource(BaseResource[SyncClient]):
    """Manages chat messages within a character session (sync).

    Provides real-time SSE streaming, non-streaming convenience wrappers,
    voice synthesis, and message history retrieval.

    Accessible via ``client.characters.chats.messages``.
    """

    def _build_payload(
        self,
        content: str,
        session_id: str,
        *,
        character_name: str | None,
        model: str,
        messages: Message | None,
        history: HistoryInput | None,
        history_server_compat: bool,
        vip_only: int,
        support_voice: int,
        refer: str,
        stream: bool,
        ss: int | None,
    ) -> dict[str, Any]:
        """Build the JSON request body for a character chat call.

        Normalizes ``history`` into a list of ``HistoryMessage`` objects and
        delegates to :func:`build_character_chat_body`. If ``messages`` is
        ``None``, wraps ``content`` in a default ``Message(role="text", ...)``.

        Returns:
            A dict ready to pass as ``json=`` to the API request.
        """
        history_message = normalize_history(history, history_server_compat)
        body = build_character_chat_body(
            content=content,
            session_id=session_id,
            vip_only=vip_only,
            support_voice=support_voice,
            name=character_name,
            model=model,
            refer=refer,
            stream=stream,
            messages=Message(role="text", content=content)
            if messages is None
            else messages,
            history_message=history_message,
            ss=ss,
        )
        return body.model_dump(exclude_none=True)

    def _stream_inner(
        self,
        route: Route,
        payload: dict[str, Any],
    ) -> Iterator[StreamItem]:
        """Drive the SSE loop for a character chat endpoint.

        Parses each raw SSE event from ``route`` and yields typed items:
        ``"chunk"`` events are validated into ``CharacterChatCompletionChunk``;
        ``"end"`` events are validated into ``CharacterChatCompletionEnd`` and
        terminate the loop. Parse errors are logged as warnings and skipped —
        the stream continues rather than raising.

        Args:
            route: The API route to stream from.
            payload: The JSON request body.

        Yields:
            ``CharacterChatCompletionChunk`` for each text delta, followed by
            a single ``CharacterChatCompletionEnd`` when the server signals
            completion.
        """
        for env in self._client.request_route_stream(
            route,
            json=payload,
            parser=self._client.parse_sse_stream,
        ):
            if env.event == "chunk":
                try:
                    yield CharacterChatCompletionChunk.model_validate(env)
                except Exception:
                    logger.warning(
                        "Failed to parse stream chunk: %s", env, exc_info=True
                    )
                    continue
            elif env.event == "end":
                yield CharacterChatCompletionEnd.model_validate(env)
                break

    def stream(
        self,
        content: str,
        session_id: str,
        *,
        character_name: str | None = None,
        model: str = "Gemini-2.5-Flash-Preview-05-20",
        messages: Message | None = None,
        history: HistoryInput | None = None,
        history_server_compat: bool = True,
        vip_only: int = 2,
        support_voice: int = 1,
        refer: str = "v1",
        stream: bool = True,
        ss: int | None = None,
    ) -> Iterator[StreamItem]:
        """Stream a message to the character and receive chunks in real time.

        Connects to the authenticated streaming endpoint and yields typed SSE
        events as they arrive. Each ``CharacterChatCompletionChunk`` carries an
        incremental text delta; the final ``CharacterChatCompletionEnd`` signals
        completion and contains token usage data.

        Args:
            content: The user message text to send.
            session_id: The chat session identifier.
            character_name: Optional display name override for the character.
            model: The AI model to use for generation.
            messages: Explicit ``Message`` payload. When ``None``, ``content``
                is automatically wrapped in ``Message(role="text", ...)``.
            history: Prior conversation history. Use ``ChatHistory`` from
                ``seaart.helpers`` (recommended) or pass a ``HistoryMessage``,
                ``dict``, ``(role, content[, msg_id])`` tuple, ``str``, or
                any iterable of these.
            history_server_compat: When ``True`` (default), ensures the history
                list ends with an assistant turn. If the last entry is not from
                the assistant, the server silently ignores the entire history;
                this flag prevents that by appending a synthetic
                ``{role: 2, content: " "}`` entry when necessary.
            vip_only: Internal access tier parameter forwarded to the API.
                Defaults to ``2``.
            support_voice: Internal voice support parameter forwarded to the API.
                Defaults to ``1``.
            refer: Internal API referrer tag. Leave at the default unless you
                have a specific reason to override it.
            stream: Server-side streaming flag. Keep ``True`` for this method.
            ss: Optional internal session-state parameter.

        Yields:
            ``CharacterChatCompletionChunk`` — one or more text deltas.
            ``CharacterChatCompletionEnd`` — final event; marks stream completion.

        Example:
            >>> from seaart.helpers import ChatHistory
            >>> history = ChatHistory().user("Hello!").ai("Hi, how can I help?")
            >>> for item in client.characters.chats.messages.stream(
            ...     "Tell me a story", session_id="abc123", history=history
            ... ):
            ...     if isinstance(item, CharacterChatCompletionChunk):
            ...         print(item.content, end="", flush=True)
        """
        payload = self._build_payload(
            content=content,
            session_id=session_id,
            character_name=character_name,
            model=model,
            messages=messages,
            history=history,
            history_server_compat=history_server_compat,
            vip_only=vip_only,
            support_voice=support_voice,
            refer=refer,
            stream=stream,
            ss=ss,
        )
        yield from self._stream_inner(Characters.Chat.Messages.STREAM, payload)

    def stream_raw(
        self,
        content: str,
        session_id: str,
        *,
        character_name: str | None = None,
        model: str = "Gemini-2.5-Flash-Preview-05-20",
        messages: Message | None = None,
        history: HistoryInput | None = None,
        history_server_compat: bool = True,
        vip_only: int = 2,
        support_voice: int = 1,
        refer: str = "v1",
        stream: bool = True,
        ss: int | None = None,
    ) -> Iterator[SimpleNamespace]:
        """Stream raw (unvalidated) SSE events from the character endpoint.

        Yields ``SimpleNamespace`` objects directly from the SSE parser without
        any Pydantic validation. Each namespace has at least ``.event`` (str),
        ``.data`` (dict), and ``.headers`` (dict) attributes.

        Useful for debugging, inspecting undocumented fields, or building custom
        parsers on top of the raw event stream.

        Args:
            content: The user message text to send.
            session_id: The chat session identifier.
            character_name: Optional display name override for the character.
            model: The AI model to use for generation.
            messages: Explicit ``Message`` payload. When ``None``, ``content``
                is automatically wrapped in ``Message(role="text", ...)``.
            history: Prior conversation history. See :meth:`stream` for the
                full list of accepted formats.
            history_server_compat: Append a trailing assistant entry when the
                history does not already end with one; the server silently
                ignores history that ends with a user message.
            vip_only: Internal access tier parameter forwarded to the API.
                Defaults to ``2``.
            support_voice: Internal voice support parameter forwarded to the API.
                Defaults to ``1``.
            refer: Internal API referrer tag.
            stream: Server-side streaming flag. Keep ``True``.
            ss: Optional internal session-state parameter.

        Yields:
            ``SimpleNamespace`` — one raw SSE event per iteration, including
            both ``"chunk"`` and ``"end"`` events.

        Example:
            >>> for ev in client.characters.chats.messages.stream_raw(
            ...     "Hello", session_id="abc123"
            ... ):
            ...     print(ev.event, ev.data)
        """
        payload = self._build_payload(
            content=content,
            session_id=session_id,
            character_name=character_name,
            model=model,
            messages=messages,
            history=history,
            history_server_compat=history_server_compat,
            vip_only=vip_only,
            support_voice=support_voice,
            refer=refer,
            stream=stream,
            ss=ss,
        )
        yield from self._client.request_route_stream(
            Characters.Chat.Messages.STREAM,
            json=payload,
            parser=self._client.parse_sse_stream,
        )

    def stream_tourist(
        self,
        content: str,
        session_id: str,
        *,
        character_name: str | None = None,
        model: str = "Gemini-2.5-Flash-Preview-05-20",
        messages: Message | None = None,
        history: HistoryInput | None = None,
        history_server_compat: bool = True,
        vip_only: int = 2,
        support_voice: int = 1,
        refer: str = "v1",
        stream: bool = True,
        ss: int | None = None,
    ) -> Iterator[StreamItem]:
        """Stream a message using the tourist (unauthenticated) endpoint.

        Functionally identical to :meth:`stream`, but routes the request through
        the public tourist endpoint which does not require an authenticated
        session token. Tourist sessions have limited free message quotas.

        Args:
            content: The user message text to send.
            session_id: The chat session identifier.
            character_name: Optional display name override for the character.
            model: The AI model to use for generation.
            messages: Explicit ``Message`` payload. When ``None``, ``content``
                is automatically wrapped in ``Message(role="text", ...)``.
            history: Prior conversation history. See :meth:`stream` for the
                full list of accepted formats.
            history_server_compat: Append a trailing assistant entry when the
                history does not already end with one; the server silently
                ignores history that ends with a user message.
            vip_only: Internal access tier parameter forwarded to the API.
                Defaults to ``2``.
            support_voice: Internal voice support parameter forwarded to the API.
                Defaults to ``1``.
            refer: Internal API referrer tag.
            stream: Server-side streaming flag. Keep ``True``.
            ss: Optional internal session-state parameter.

        Yields:
            ``CharacterChatCompletionChunk`` — one or more text deltas.
            ``CharacterChatCompletionEnd`` — final event; marks stream completion.

        Example:
            >>> for item in client.characters.chats.messages.stream_tourist(
            ...     "Hi!", session_id="abc123"
            ... ):
            ...     if isinstance(item, CharacterChatCompletionChunk):
            ...         print(item.content, end="", flush=True)
        """
        payload = self._build_payload(
            content=content,
            session_id=session_id,
            character_name=character_name,
            model=model,
            messages=messages,
            history=history,
            history_server_compat=history_server_compat,
            vip_only=vip_only,
            support_voice=support_voice,
            refer=refer,
            stream=stream,
            ss=ss,
        )
        yield from self._stream_inner(Characters.Chat.Messages.STREAM_TOURIST, payload)

    def stream_tourist_raw(
        self,
        content: str,
        session_id: str,
        *,
        character_name: str | None = None,
        model: str = "Gemini-2.5-Flash-Preview-05-20",
        messages: Message | None = None,
        history: HistoryInput | None = None,
        history_server_compat: bool = True,
        vip_only: int = 2,
        support_voice: int = 1,
        refer: str = "v1",
        stream: bool = True,
        ss: int | None = None,
    ) -> Iterator[SimpleNamespace]:
        """Stream raw (unvalidated) SSE events from the tourist endpoint.

        Identical to :meth:`stream_raw` but uses the public tourist endpoint.
        Yields ``SimpleNamespace`` objects directly from the SSE parser without
        any Pydantic validation.

        Args:
            content: The user message text to send.
            session_id: The chat session identifier.
            character_name: Optional display name override for the character.
            model: The AI model to use for generation.
            messages: Explicit ``Message`` payload. When ``None``, ``content``
                is automatically wrapped in ``Message(role="text", ...)``.
            history: Prior conversation history. See :meth:`stream` for the
                full list of accepted formats.
            history_server_compat: Append a trailing assistant entry when the
                history does not already end with one; the server silently
                ignores history that ends with a user message.
            vip_only: Internal access tier parameter forwarded to the API.
                Defaults to ``2``.
            support_voice: Internal voice support parameter forwarded to the API.
                Defaults to ``1``.
            refer: Internal API referrer tag.
            stream: Server-side streaming flag. Keep ``True``.
            ss: Optional internal session-state parameter.

        Yields:
            ``SimpleNamespace`` — one raw SSE event per iteration.
        """
        payload = self._build_payload(
            content=content,
            session_id=session_id,
            character_name=character_name,
            model=model,
            messages=messages,
            history=history,
            history_server_compat=history_server_compat,
            vip_only=vip_only,
            support_voice=support_voice,
            refer=refer,
            stream=stream,
            ss=ss,
        )
        yield from self._client.request_route_stream(
            Characters.Chat.Messages.STREAM_TOURIST,
            json=payload,
            parser=self._client.parse_sse_stream,
        )

    def send_tourist(
        self,
        content: str,
        session_id: str,
        *,
        character_name: str | None = None,
        model: str = "Gemini-2.5-Flash-Preview-05-20",
        messages: Message | None = None,
        history: HistoryInput | None = None,
        history_server_compat: bool = True,
        vip_only: int = 2,
        support_voice: int = 1,
        refer: str = "v1",
        stream: bool = True,
        ss: int | None = None,
    ) -> CharacterChatCompletionResult:
        """Send a message via the tourist endpoint and collect the full response.

        Internally consumes :meth:`stream_tourist` and concatenates all chunk
        deltas into a single ``CharacterChatCompletionResult``. Use this when
        you need the complete reply at once rather than streaming it token by
        token, or when an authenticated session is not available.

        Args:
            content: The user message text to send.
            session_id: The chat session identifier.
            character_name: Optional display name override for the character.
            model: The AI model to use for generation.
            messages: Explicit ``Message`` payload. When ``None``, ``content``
                is automatically wrapped in ``Message(role="text", ...)``.
            history: Prior conversation history. See :meth:`stream` for the
                full list of accepted formats.
            history_server_compat: Append a trailing assistant entry when the
                history does not already end with one; the server silently
                ignores history that ends with a user message.
            vip_only: Internal access tier parameter forwarded to the API.
                Defaults to ``2``.
            support_voice: Internal voice support parameter forwarded to the API.
                Defaults to ``1``.
            refer: Internal API referrer tag.
            stream: Server-side streaming flag. Keep ``True``.
            ss: Optional internal session-state parameter.

        Returns:
            ``CharacterChatCompletionResult`` with the full reply text in
            ``.text`` and the final SSE event in ``.value``.

        Raises:
            StreamInterruptedError: If the stream closes before an ``"end"``
                event is received, indicating a dropped connection.

        Example:
            >>> result = client.characters.chats.messages.send_tourist(
            ...     "Hello!", session_id="abc123"
            ... )
            >>> print(result.text)
        """
        parts: list[str] = []
        end: CharacterChatCompletionEnd | None = None

        for item in self.stream_tourist(
            content=content,
            session_id=session_id,
            character_name=character_name,
            model=model,
            messages=messages,
            history=history,
            history_server_compat=history_server_compat,
            vip_only=vip_only,
            support_voice=support_voice,
            refer=refer,
            stream=stream,
            ss=ss,
        ):
            if isinstance(item, CharacterChatCompletionChunk):
                text = item.content
                if text:
                    parts.append(text)
            else:
                end = item

        if end is None:
            raise StreamInterruptedError(
                "Stream ended without an 'end' event — the connection may have been interrupted."
            )

        return CharacterChatCompletionResult(text="".join(parts), value=end)

    def send(
        self,
        content: str,
        session_id: str,
        *,
        character_name: str | None = None,
        model: str = "Gemini-2.5-Flash-Preview-05-20",
        messages: Message | None = None,
        history: HistoryInput | None = None,
        history_server_compat: bool = True,
        vip_only: int = 2,
        support_voice: int = 1,
        refer: str = "v1",
        stream: bool = True,
        ss: int | None = None,
    ) -> CharacterChatCompletionResult:
        """Send a message and collect the full response.

        Non-streaming convenience wrapper around :meth:`stream`. Internally
        consumes all SSE chunks and concatenates the text deltas into a single
        ``CharacterChatCompletionResult``.

        Args:
            content: The user message text to send.
            session_id: The chat session identifier.
            character_name: Optional display name override for the character.
            model: The AI model to use for generation.
            messages: Explicit ``Message`` payload. When ``None``, ``content``
                is automatically wrapped in ``Message(role="text", ...)``.
            history: Prior conversation history. See :meth:`stream` for the
                full list of accepted formats.
            history_server_compat: Append a trailing assistant entry when the
                history does not already end with one; the server silently
                ignores history that ends with a user message.
            vip_only: Internal access tier parameter forwarded to the API.
                Defaults to ``2``.
            support_voice: Internal voice support parameter forwarded to the API.
                Defaults to ``1``.
            refer: Internal API referrer tag.
            stream: Server-side streaming flag. Keep ``True``.
            ss: Optional internal session-state parameter.

        Returns:
            ``CharacterChatCompletionResult`` with the full reply text in
            ``.text`` and the final SSE event in ``.value``.

        Raises:
            StreamInterruptedError: If the stream closes before an ``"end"``
                event is received, indicating a dropped connection.

        Example:
            >>> from seaart.helpers import ChatHistory
            >>> history = ChatHistory()
            >>> result = client.characters.chats.messages.send(
            ...     "Hello!", session_id="abc123"
            ... )
            >>> history.user("Hello!").add(result)  # appends assistant reply
            >>> result2 = client.characters.chats.messages.send(
            ...     "Tell me a joke", session_id="abc123", history=history
            ... )
            >>> print(result2.text)
        """
        parts: list[str] = []
        end: CharacterChatCompletionEnd | None = None

        for item in self.stream(
            content=content,
            session_id=session_id,
            character_name=character_name,
            model=model,
            messages=messages,
            history=history,
            history_server_compat=history_server_compat,
            vip_only=vip_only,
            support_voice=support_voice,
            refer=refer,
            stream=stream,
            ss=ss,
        ):
            if isinstance(item, CharacterChatCompletionChunk):
                text = item.content
                if text:
                    parts.append(text)
            else:
                end = item

        if end is None:
            raise StreamInterruptedError(
                "Stream ended without an 'end' event — the connection may have been interrupted."
            )

        return CharacterChatCompletionResult(text="".join(parts), value=end)

    def stream_voice(
        self,
        msg_id: str,
        *,
        sub_msg_id: str | None = None,
        timbre_id: str = "eba3e397c5d5dc7e0324fdd267c8e0ff",
    ) -> Iterator[VoiceStreamItem]:
        """Stream raw voice synthesis chunks for a character message.

        Connects to the voice SSE endpoint and yields typed audio events.
        ``"chunk"`` events carry base64-encoded audio fragments;
        the ``"end"`` event confirms synthesis completion and contains
        metadata such as duration and voice ID.

        Prefer :meth:`get_audio` or :meth:`save_audio` when you only need
        the final audio bytes rather than the individual streaming chunks.

        Args:
            msg_id: ID of the message to synthesize speech for. Obtained from
                ``CharacterChatCompletionChunk.my_msg_id`` or
                ``CharacterChatCompletionChunk.response_msg_id``.
            sub_msg_id: Sub-message ID. Pass ``None`` to use the primary response.
            timbre_id: Voice identifier.

        Yields:
            ``CharacterChatVoiceChunk`` — a single audio fragment with a
            base64-encoded ``audio_data`` field.
            ``CharacterChatVoiceEnd`` — final event; contains voice metadata
            such as ``voice.duration`` and ``voice.id``.

        Example:
            >>> for item in client.characters.chats.messages.stream_voice(
            ...     msg_id="msg_abc123"
            ... ):
            ...     if isinstance(item, CharacterChatVoiceChunk):
            ...         print("chunk:", len(item.audio_data), "chars")
        """
        body = build_character_voice_chat_body(
            msg_id=msg_id,
            sub_msg_id=sub_msg_id,
            timbre_id=timbre_id,
        )
        payload: dict[str, Any] = body.model_dump(exclude_none=True)

        for env in self._client.request_route_stream(
            Characters.Chat.Messages.STREAM_VOICE,
            json=payload,
            parser=self._client.parse_sse_stream,
        ):
            if env.event == "chunk":
                chunk = CharacterChatVoiceChunk.model_validate(env)
                yield chunk
            elif env.event == "end":
                end = CharacterChatVoiceEnd.model_validate(env)
                yield end
                break

    def get_audio(
        self,
        msg_id: str,
        *,
        sub_msg_id: str | None = None,
        timbre_id: str = "eba3e397c5d5dc7e0324fdd267c8e0ff",
    ) -> bytes | None:
        """Fetch synthesized voice audio for a message as raw bytes.

        Collects all chunks from :meth:`stream_voice`, strips whitespace from
        the concatenated base64 string, and decodes it to binary. Returns
        ``None`` if the server yields no audio data.

        Args:
            msg_id: ID of the message to synthesize speech for.
            sub_msg_id: Sub-message ID. Pass ``None`` to use the primary response.
            timbre_id: Voice identifier.

        Returns:
            Raw MP3 audio bytes, or ``None`` if no audio was received.

        Example:
            >>> audio = client.characters.chats.messages.get_audio("msg_abc123")
            >>> if audio:
            ...     Path("reply.mp3").write_bytes(audio)
        """
        parts: list[str] = []

        for chunk in self.stream_voice(
            msg_id=msg_id,
            sub_msg_id=sub_msg_id,
            timbre_id=timbre_id,
        ):
            if chunk.event == "chunk":
                assert isinstance(chunk, CharacterChatVoiceChunk)
                parts.append(chunk.audio_data)

        if not parts:
            return None

        cleaned = "".join("".join(parts).split())
        return base64.b64decode(cleaned)

    def save_audio(
        self,
        msg_id: str,
        *,
        sub_msg_id: str | None = None,
        timbre_id: str = "eba3e397c5d5dc7e0324fdd267c8e0ff",
        file_path: str | Path | None = None,
    ) -> Path | None:
        """Fetch synthesized voice audio for a message and save it to disk.

        Delegates audio retrieval to :meth:`get_audio`, then writes the bytes
        to the resolved destination path. Returns ``None`` (without writing)
        if no audio was received from the server.

        Path resolution rules:

        * ``file_path=None`` — saves as ``voice_{msg_id}.mp3`` in the current
          working directory.
        * ``file_path`` points to an existing directory — saves as
          ``voice_{msg_id}.mp3`` inside that directory.
        * ``file_path`` is a full file path — saves there; parent directories
          are created automatically if they do not exist.

        Args:
            msg_id: ID of the message to synthesize speech for.
            sub_msg_id: Sub-message ID. Pass ``None`` to use the primary response.
            timbre_id: Voice identifier.
            file_path: Destination file or directory path. See path resolution
                rules above.

        Returns:
            The ``Path`` the audio was written to, or ``None`` if no audio
            was received.

        Example:
            >>> path = client.characters.chats.messages.save_audio(
            ...     "msg_abc123", file_path="/tmp/voices/"
            ... )
            >>> if path:
            ...     print(f"Saved to {path}")
        """
        audio = self.get_audio(
            msg_id=msg_id,
            sub_msg_id=sub_msg_id,
            timbre_id=timbre_id,
        )
        if audio is None:
            return None

        p: Path
        if file_path is None:
            p = Path(f"voice_{msg_id}.mp3")
        else:
            p = Path(file_path)
            if p.is_dir():
                p = p / f"voice_{msg_id}.mp3"
            else:
                p.parent.mkdir(parents=True, exist_ok=True)

        p.write_bytes(audio)
        return p

    def list(
        self,
        session_id: str,
        *,
        offset_to: int = 0,
        page_size: int = 20,
        send_ss: int | None = None,
        ss: int | None = None,
    ) -> APIEnvelope[HistoryCharacterMessages]:
        """Retrieve paginated message history for a character chat session.

        Args:
            session_id: The session whose history to retrieve.
            offset_to: Pagination cursor — the message offset to start from.
                Pass ``0`` (default) to fetch from the beginning of the session.
            page_size: Maximum number of messages to return per page.
                Defaults to ``20``.
            send_ss: Internal session-state parameter forwarded to the API.
            ss: Internal session-state parameter forwarded to the API.

        Returns:
            ``APIEnvelope[HistoryCharacterMessages]`` — call ``.value`` to
            access the result. ``.value.items`` is a list of ``Item`` objects
            (each with ``.id``, ``.content``, ``.role``, etc.).
            ``.value.has_more`` indicates whether additional pages exist.

        Example:
            >>> envelope = client.characters.chats.messages.list(
            ...     "session_abc123", page_size=50
            ... )
            >>> for msg in envelope.value.items:
            ...     speaker = "user" if msg.role == 1 else "character"
            ...     print(f"[{speaker}] {msg.content}")
        """
        body = HistoryCharacterMessagesBody(
            session_id=session_id,
            offset_to=offset_to,
            page_size=page_size,
            send_ss=send_ss,
            ss=ss,
        )
        env = self._client.request_route(
            Characters.Chat.Messages.LIST,
            json=body.model_dump(exclude_none=True),
        )
        return APIEnvelope[HistoryCharacterMessages].model_validate(env)
