from collections.abc import Iterable as AbcIterable
from collections.abc import Mapping
from typing import Any, cast

from curl_cffi import CurlMime

from ._internal._schemas import MimePart
from ._internal._schemas.characters.characters_chat import HistoryMessage
from ._types import HistoryInput, HistoryRole


def mask_token(token: str) -> str:
    return token[:5] + "..." + token[-4:]


def mask_sensitive_json(json: dict[str, Any]) -> dict[str, Any]:
    safe = json.copy()
    if "password" in safe:
        safe["password"] = "***"  # noqa: S105
    return safe


def make_multipart(parts: list[MimePart]) -> CurlMime:
    return CurlMime.from_list(parts)  # type: ignore[arg-type, no-any-return]


def _role_to_int(role: HistoryRole) -> int:
    """Normalise a flexible role value to ``1`` (user) or ``2`` (assistant).

    Args:
        role: An integer ``1``/``2``, or a string alias such as ``"user"``,
            ``"assistant"``, ``"human"``, ``"ai"``, ``"model"``, ``"bot"``.

    Returns:
        ``1`` for user roles, ``2`` for assistant roles.

    Raises:
        ValueError: If the value cannot be mapped to a known role.
    """
    if isinstance(role, int):
        if role in (1, 2):
            return role
        raise ValueError(f"role must be 1 or 2, got {role}")

    r = str(role).strip().lower()
    if r in ("user", "u", "human", "client", "1"):
        return 1
    if r in ("assistant", "ai", "model", "bot", "2"):
        return 2
    raise ValueError(f"Unknown role {role!r}. Use 1/2 or 'user'/'assistant'.")


def normalize_history(
    history: HistoryInput, ensure_last_assistant: bool = True
) -> list[HistoryMessage]:
    """Convert flexible history input to a list of ``HistoryMessage`` objects.

    Accepts a wide range of input formats so callers are not forced to build
    ``HistoryMessage`` instances manually.  Supported item types:

    - ``HistoryMessage`` — used as-is.
    - ``str`` — treated as a user message (role ``1``).
    - ``tuple[role, content]`` or ``tuple[role, content, msg_id]``.
    - ``dict`` with keys ``"role"``, ``"content"``, and optionally ``"msg_id"``.
    - Any object with ``content`` and optionally ``role`` / ``msg_id`` attributes.

    Args:
        history: One item or an iterable of items in any of the formats above.
            Pass ``None`` to get an empty list.
        ensure_last_assistant: If ``True`` (default) and the last message is not
            from the assistant, a blank assistant placeholder is appended.  This
            satisfies the API requirement that history must end with an assistant
            turn.

    Returns:
        Normalised list of ``HistoryMessage`` objects ready to pass to the API.

    Raises:
        TypeError: If an item cannot be interpreted as a history message.
        ValueError: If a role value cannot be mapped to ``1`` or ``2``.
    """
    if history is None:
        return []

    if isinstance(history, (HistoryMessage, str, tuple, dict)):
        items: list[object] = [history]
    else:
        items = list(cast("AbcIterable[object]", history))

    out: list[HistoryMessage] = []

    for it in items:
        if isinstance(it, HistoryMessage):
            out.append(it)
            continue

        if isinstance(it, str):
            out.append(HistoryMessage(role=1, content=it, msg_id=""))
            continue

        if isinstance(it, tuple):
            t = cast("tuple[object, ...]", it)

            if len(t) == 2:
                role_raw = cast("HistoryRole", t[0])
                content_raw = t[1]

                role = _role_to_int(role_raw)
                if not isinstance(content_raw, str):
                    raise TypeError("Tuple history item: content must be str")

                out.append(HistoryMessage(role=role, content=content_raw, msg_id=""))
                continue

            if len(t) == 3:
                role_raw = cast("HistoryRole", t[0])
                content_raw = t[1]
                msg_id_raw = t[2]

                role = _role_to_int(role_raw)
                if not isinstance(content_raw, str):
                    raise TypeError("Tuple history item: content must be str")
                if not isinstance(msg_id_raw, str):
                    raise TypeError("Tuple history item: msg_id must be str")

                out.append(
                    HistoryMessage(role=role, content=content_raw, msg_id=msg_id_raw)
                )
                continue

            raise TypeError(
                "Tuple history item must be (role, content) or (role, content, msg_id)"
            )

        if isinstance(it, Mapping):
            m = cast("Mapping[str, Any]", it)

            role = _role_to_int(cast("HistoryRole", m.get("role", 1)))

            content_raw = m.get("content", "")
            if not isinstance(content_raw, str):
                raise TypeError("dict history item: 'content' must be str")

            msg_id_raw = m.get("msg_id", "")
            if not isinstance(msg_id_raw, str):
                raise TypeError("dict history item: 'msg_id' must be str")

            out.append(
                HistoryMessage(role=role, content=content_raw, msg_id=msg_id_raw or "")
            )
            continue

        content_attr = getattr(it, "content", None)
        if isinstance(content_attr, str):
            role_attr = cast("HistoryRole", getattr(it, "role", 2))
            role = _role_to_int(role_attr)

            msg_id_attr = getattr(it, "msg_id", None)
            if msg_id_attr is None:
                msg_id_attr = getattr(it, "id", "")
            if not isinstance(msg_id_attr, str):
                msg_id_attr = ""

            out.append(
                HistoryMessage(role=role, content=content_attr, msg_id=msg_id_attr)
            )
            continue

        raise TypeError(f"Unsupported history item type: {type(it)!r}")

    if ensure_last_assistant and out and out[-1].role != 2:
        out.append(HistoryMessage(role=2, content=" ", msg_id=""))

    return out
