"""QQ Bot channel adapter."""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

from beaver.foundation.events import InboundMessage, OutboundMessage
from beaver.interfaces.channels.base import ChannelInboundSink

from .base import (
    allowed_by_policy,
    build_inbound_message,
    chunk_text,
    compact_media_summary,
    config_list,
    mark_unclaimed,
    outbound_target,
)

EventRecorder = Callable[..., None]


class QQBotAdapter:
    """QQ Bot API adapter with injectable client support."""

    KIND = "qqbot"

    def __init__(
        self,
        *,
        channel_id: str,
        kind: str,
        mode: str,
        account_id: str,
        display_name: str | None,
        inbound_sink: ChannelInboundSink,
        secrets: dict[str, Any] | None = None,
        config: dict[str, Any] | None = None,
        event_recorder: EventRecorder | None = None,
        client: Any | None = None,
    ) -> None:
        self.channel_id = channel_id
        self.kind = kind
        self.mode = mode
        self.account_id = account_id
        self.display_name = display_name
        self.inbound_sink = inbound_sink
        self.secrets = secrets or {}
        self.config = config or {}
        self.event_recorder = event_recorder
        self._client = client
        self.max_message_chars = int(self.config.get("maxMessageChars") or 2000)

    async def start(self) -> None:
        if self._client is not None:
            return
        if self.mode != "websocket":
            raise ValueError(f"Unsupported qqbot mode: {self.mode}")
        self._client = self._build_client()

    async def stop(self) -> None:
        close = getattr(self._client, "close", None)
        if close is not None:
            result = close()
            if hasattr(result, "__await__"):
                await result

    async def handle_event_payload(self, payload: dict[str, Any]) -> None:
        message = self._normalize_payload(payload)
        if message is None:
            return
        await self.inbound_sink.accept_inbound(message)

    async def send(self, message: OutboundMessage) -> None:
        target = outbound_target(message)
        if not target.peer_id:
            mark_unclaimed(message)
            return
        client = self._require_client()
        platform_message_id = message.channel_identity.message_id if message.channel_identity else None
        for chunk in chunk_text(message.content, max_chars=self.max_message_chars):
            await client.send_text(
                peer_type=target.peer_type,
                peer_id=target.peer_id,
                content=chunk,
                message_id=platform_message_id,
            )

    def _normalize_payload(self, payload: dict[str, Any]) -> InboundMessage | None:
        event_type = str(payload.get("t") or payload.get("type") or "")
        data = payload.get("d") if isinstance(payload.get("d"), dict) else payload
        author = data.get("author") if isinstance(data.get("author"), dict) else {}

        route = self._route(event_type, data, author)
        if route is None:
            return None
        peer_id, peer_type, user_id, thread_id = route

        if peer_type == "dm":
            if not allowed_by_policy(
                policy=self.config.get("dmPolicy"),
                identifier=user_id or peer_id,
                allowlist=config_list(self.config, "allowFrom"),
                default="open",
            ):
                return None
        elif peer_type == "group":
            if not allowed_by_policy(
                policy=self.config.get("groupPolicy"),
                identifier=peer_id,
                allowlist=config_list(self.config, "groupAllowFrom"),
                default="open",
            ):
                return None

        message_id = _string_or_none(data.get("id"))
        content = str(data.get("content") or "").strip()
        media_entries = self._media_entries(data)
        if media_entries:
            content = "\n".join([part for part in [content, *media_entries] if part]).strip()
        if not content:
            return None

        metadata = {
            "event_type": event_type,
            "message_id": message_id,
            "peer_type": peer_type,
        }
        if media_entries:
            metadata["media"] = media_entries

        return build_inbound_message(
            channel_id=self.channel_id,
            kind=self.kind,
            account_id=self.account_id,
            peer_id=peer_id,
            thread_id=thread_id,
            peer_type=peer_type,
            user_id=user_id,
            message_id=message_id,
            content=content,
            metadata=metadata,
        )

    def _route(
        self,
        event_type: str,
        data: dict[str, Any],
        author: dict[str, Any],
    ) -> tuple[str, str, str | None, str | None] | None:
        if event_type == "C2C_MESSAGE_CREATE":
            peer_id = _string_or_none(author.get("user_openid"))
            if not peer_id:
                return None
            return peer_id, "dm", peer_id, None
        if event_type == "GROUP_AT_MESSAGE_CREATE":
            peer_id = _string_or_none(data.get("group_openid"))
            if not peer_id:
                return None
            return peer_id, "group", _string_or_none(author.get("member_openid")), None
        if data.get("guild_id") and data.get("channel_id"):
            peer_id = _string_or_none(data.get("channel_id"))
            if not peer_id:
                return None
            return peer_id, "channel", _string_or_none(author.get("id")), _string_or_none(data.get("guild_id"))
        return None

    def _media_entries(self, data: dict[str, Any]) -> list[str]:
        entries: list[str] = []
        attachments = data.get("attachments")
        if not isinstance(attachments, list):
            return entries
        for attachment in attachments:
            if not isinstance(attachment, dict):
                continue
            media_type = str(attachment.get("content_type") or attachment.get("type") or "attachment")
            entries.append(compact_media_summary(media_type, file_name=_string_or_none(attachment.get("filename"))))
        return entries

    def _require_client(self) -> Any:
        if self._client is None:
            self._client = self._build_client()
        return self._client

    def _build_client(self) -> Any:
        self._require_secret("appId")
        self._require_secret("clientSecret")
        try:
            import aiohttp  # noqa: F401
        except ImportError as exc:  # pragma: no cover - optional live dependency
            raise RuntimeError("Install beaver-backend[qqbot] to enable QQBotAdapter") from exc
        raise RuntimeError("QQBot live client is not configured for direct construction")

    def _require_secret(self, key: str) -> str:
        value = self.secrets.get(key)
        if not value:
            raise ValueError(f"{key} is required")
        return str(value)


def _string_or_none(value: Any) -> str | None:
    if value is None:
        return None
    text = str(value).strip()
    return text or None
