from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from urllib.parse import parse_qs, urlparse

from ._enums import Role
from ._internal._schemas.characters.characters_messages import (
    CharacterChatCompletionResult,
)

if TYPE_CHECKING:
    from collections.abc import Iterator

    from ._types import HistoryInput, HistoryItem

_TASK_ID_RE = re.compile(r"^[a-z][a-z0-9]{14,}$")


@dataclass
class ChatHistory:
    """Mutable, fluent builder for a character chat conversation history.

    Stores messages as ``(role, content, msg_id)`` tuples compatible with
    :func:`normalize_history`.  All mutating methods return ``self`` to allow
    method chaining.

    Example:
        >>> history = ChatHistory()
        >>> history.user("Hello!").ai("Hi there!")
        ChatHistory(...)
        >>> len(history)
        2
    """

    _items: list[HistoryItem] = field(default_factory=lambda: [])  # noqa: PIE807

    def user(self, text: str, msg_id: str = "") -> ChatHistory:
        """Append a user message.

        Args:
            text: Message content.
            msg_id: Optional message ID (default ``""``).

        Returns:
            ``self`` for chaining.
        """
        self._items.append((Role.USER, text, msg_id))
        return self

    def ai(self, text: str, msg_id: str = "") -> ChatHistory:
        """Append an assistant message.

        Args:
            text: Message content.
            msg_id: Optional message ID (default ``""``).

        Returns:
            ``self`` for chaining.
        """
        self._items.append((Role.ASSISTANT, text, msg_id))
        return self

    def add(
        self,
        source: str | CharacterChatCompletionResult | ChatHistory,
        role: Role | None = None,
        msg_id: str = "",
    ) -> ChatHistory:
        """Append one or more messages from a flexible source.

        Args:
            source: A plain string (requires ``role``), a
                ``CharacterChatCompletionResult`` (appended as an assistant
                message), or another ``ChatHistory`` (all its items are merged).
            role: Required when ``source`` is a ``str``; must be
                ``Role.USER`` or ``Role.ASSISTANT``.
            msg_id: Optional message ID used when ``source`` is a ``str``.

        Returns:
            ``self`` for chaining.

        Raises:
            ValueError: If ``source`` is a ``str`` and ``role`` is ``None`` or
                not a valid ``Role``.
        """
        if isinstance(source, ChatHistory):
            self._items.extend(source._items)
            return self

        if isinstance(source, CharacterChatCompletionResult):
            return self.ai(
                source.value.content,
                msg_id=source.value.headers.x_response_msg_id or msg_id,
            )

        if role is None:
            raise ValueError("role is required when source is str")
        if role == Role.USER:
            return self.user(source, msg_id=msg_id)
        if role == Role.ASSISTANT:
            return self.ai(source, msg_id=msg_id)
        raise ValueError("role must be Role.USER or Role.ASSISTANT")

    def delete_last(self, n: int = 1) -> ChatHistory:
        """Remove the last ``n`` messages.

        Args:
            n: Number of messages to remove (default ``1``).  Non-positive
                values are ignored.

        Returns:
            ``self`` for chaining.
        """
        if n > 0:
            del self._items[-n:]
        return self

    def clear(self) -> ChatHistory:
        """Remove all messages.

        Returns:
            ``self`` for chaining.
        """
        self._items.clear()
        return self

    def copy(self) -> ChatHistory:
        """Return a shallow copy of this history.

        Returns:
            A new ``ChatHistory`` with the same items.
        """
        new = ChatHistory()
        new._items = self._items.copy()
        return new

    def to_input(self) -> HistoryInput:
        """Return the underlying item list as a :data:`HistoryInput`.

        Returns:
            The raw list of ``(role, content, msg_id)`` tuples.
        """
        return self._items

    def __iter__(self) -> Iterator[HistoryItem]:
        return iter(self._items)

    def __len__(self) -> int:
        return len(self._items)

    def __repr__(self) -> str:
        return f"ChatHistory({self._items!r})"

    def __iadd__(self, other: ChatHistory) -> ChatHistory:
        self._items.extend(other._items)
        return self

    def __add__(self, other: ChatHistory) -> ChatHistory:
        return self.copy().add(other)


@dataclass(frozen=True, slots=True)
class CharacterLink:
    """Parsed components of a SeaArt character URL.

    Attributes:
        character_id: Unique identifier of the character.
        session_id: Chat session ID extracted from the ``s_id`` query parameter,
            or ``None`` if absent.
    """

    character_id: str
    session_id: str | None = None


def extract_character_link(url: str) -> CharacterLink | None:
    """Extract ``character_id`` and optional ``session_id`` from a SeaArt character URL.

    Supported formats:

    - ``https://www.seaart.ai/character/chat/<character_id>?s_id=<session_id>``
    - ``https://www.seaart.ai/character/details/<character_id>``

    Args:
        url: A SeaArt character chat or details URL.

    Returns:
        A :class:`CharacterLink` with the parsed components, or ``None`` if
        the URL does not match a recognised format.
    """
    parsed = urlparse(url.strip())

    path = (parsed.path or "").strip("/")
    parts = [p for p in path.split("/") if p]

    try:
        character_idx = parts.index("character")
    except ValueError:
        return None

    if character_idx + 2 >= len(parts):
        return None
    if parts[character_idx + 1] not in {"chat", "details"}:
        return None

    character_id = parts[character_idx + 2].strip()
    if not character_id:
        return None

    qs = parse_qs(parsed.query)
    session_id = next(iter(qs.get("s_id", [])), None)

    return CharacterLink(character_id=character_id, session_id=session_id)


@dataclass(frozen=True)
class ModelLink:
    """Parsed components of a SeaArt model URL.

    Attributes:
        model_no: Unique model number.
        model_ver_no: Optional model version number, or ``None`` if absent.
    """

    model_no: str
    model_ver_no: str | None = None


def extract_model_link(url: str) -> ModelLink | None:
    """Extract ``model_no`` and optional ``model_ver_no`` from a SeaArt model URL.

    Supported formats:

    - ``https://www.seaart.ai/create/image?id=<model_no>&model_ver_no=<model_ver_no>``
    - ``https://www.seaart.ai/models/detail/<model_no>``

    Args:
        url: A SeaArt model detail or image-creation URL.

    Returns:
        A :class:`ModelLink` with the parsed components, or ``None`` if
        the URL does not match a recognised format.
    """
    parsed = urlparse(url.strip())
    path = (parsed.path or "").strip("/")
    parts = [p for p in path.split("/") if p]

    if "models" in parts and "detail" in parts:
        idx = parts.index("detail")
        if idx + 1 < len(parts):
            return ModelLink(model_no=parts[idx + 1])
        return None

    qs = parse_qs(parsed.query)
    model_no = next(iter(qs.get("id", [])), None)
    if model_no:
        model_ver_no = next(iter(qs.get("model_ver_no", [])), None)
        return ModelLink(model_no=model_no, model_ver_no=model_ver_no)

    return None


def extract_task_id_from_image_url(url: str) -> str | None:
    """Extract the task ID embedded in a SeaArt CDN image URL.

    The task ID occupies the path segment immediately before the filename.

    Args:
        url: A SeaArt CDN image URL.

    Returns:
        The task ID string, or ``None`` if the URL structure does not contain
        a recognisable task ID (e.g. static upload URLs).

    Example:
        >>> extract_task_id_from_image_url(
        ...     "https://image.cdn2.seaart.me/2026-02-25/d6fhvcte878c73fhuu50/d9ed9776fe07c56308c14582781c35b1_high.webp"
        ... )
        'd6fhvcte878c73fhuu50'
        >>> extract_task_id_from_image_url(
        ...     "https://image.cdn2.seaart.me/temp-convert-webp/highwebp/2025-07-04/d1k5dlte878c73c2cmu0/cfd7361544ce27c1dc02e75d451f7e7d_low.webp"
        ... )
        'd1k5dlte878c73c2cmu0'
        >>> extract_task_id_from_image_url(
        ...     "https://image.cdn2.seaart.me/upload/static/20260224/124f66c1b26f1e09d798c500aa386b03_high.webp"
        ... )
    """
    path = urlparse(url.strip()).path
    parts = [p for p in path.split("/") if p]
    if len(parts) < 2:
        return None
    candidate = parts[-2]
    return candidate if _TASK_ID_RE.match(candidate) else None


__all__ = [
    "ChatHistory",
    "extract_character_link",
    "extract_model_link",
    "extract_task_id_from_image_url",
]
