"""ACP (Agent Communication Protocol) adapter for Skytale encrypted channels.

Maps IBM's ACP protocol concepts to Skytale's MLS-encrypted channel
infrastructure.  ACP uses task-based routing: each ACP ``taskId`` becomes a
Skytale channel at ``org/acp/{task_id}``, and ACP message dicts are
JSON-serialized, wrapped in an :class:`~skytale_sdk.envelope.Envelope` tagged
with ``Protocol.ACP``, and transmitted through MLS-encrypted channels.

The adapter works with plain Python dicts today; an ``acp-sdk`` dependency is
**not** required.  If/when we add direct ``acp-sdk`` type support, it will be
imported lazily inside the methods that need it.

Usage::

    from skytale_sdk.channels import SkytaleChannelManager
    from skytale_sdk.integrations._acp import SkytaleACPAdapter

    mgr = SkytaleChannelManager(identity=b"my-agent")
    adapter = SkytaleACPAdapter(mgr, agent_id="agent-007")

    adapter.create_task("analysis-42")
    adapter.send_message("analysis-42", {"status": "complete", "result": "done"})
    msgs = adapter.receive_messages("analysis-42")
"""

from __future__ import annotations

import json
import logging
import uuid
from typing import Dict, List

from skytale_sdk.channels import SkytaleChannelManager
from skytale_sdk.envelope import Envelope, Protocol

logger = logging.getLogger(__name__)


class SkytaleACPAdapter:
    """Maps ACP protocol concepts to Skytale encrypted channels.

    Each ACP task maps to a Skytale channel at ``org/acp/{task_id}``.
    Messages are serialized as JSON, wrapped in an :class:`Envelope` with
    ``Protocol.ACP``, and sent through MLS-encrypted channels.

    Args:
        manager: A :class:`SkytaleChannelManager` that owns the underlying
            encrypted channels.
        agent_id: Identifier for this agent, embedded in every outgoing ACP
            message so receivers know who sent it.

    Example::

        from skytale_sdk.channels import SkytaleChannelManager
        from skytale_sdk.integrations._acp import SkytaleACPAdapter

        mgr = SkytaleChannelManager(identity=b"acp-agent")
        adapter = SkytaleACPAdapter(mgr, agent_id="agent-42")
        adapter.create_task("analysis-42")
        adapter.send_message("analysis-42", {"status": "complete"})
    """

    def __init__(self, manager: SkytaleChannelManager, agent_id: str) -> None:
        self._manager = manager
        self._agent_id = agent_id

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _channel_name(self, task_id: str) -> str:
        """Convert an ACP task ID to a Skytale channel name.

        ACP tasks live under the ``org/acp/`` namespace so they do not
        collide with channels created by other protocols or directly by
        users.

        Args:
            task_id: The ACP task identifier.

        Returns:
            A Skytale channel name in ``org/acp/{task_id}`` format.

        Example::

            >>> adapter._channel_name("analysis-42")
            'org/acp/analysis-42'
        """
        return f"org/acp/{task_id}"

    # ------------------------------------------------------------------
    # Task lifecycle
    # ------------------------------------------------------------------

    def create_task(self, task_id: str) -> None:
        """Create a new ACP task backed by a Skytale encrypted channel.

        The underlying MLS group is created immediately and a background
        listener thread starts buffering incoming messages.

        Args:
            task_id: Unique identifier for the ACP task.

        Raises:
            RuntimeError: If channel creation fails (e.g. invalid name or
                relay unreachable).

        Example::

            adapter.create_task("analysis-42")
        """
        channel = self._channel_name(task_id)
        self._manager.create(channel)
        logger.info("created ACP task %s on channel %s", task_id, channel)

    def join_task(self, task_id: str, welcome: bytes) -> None:
        """Join an existing ACP task using an MLS Welcome message.

        Another agent must have already created the task and generated
        the Welcome via :meth:`SkytaleChannelManager.add_member`.

        Args:
            task_id: The ACP task identifier to join.
            welcome: MLS Welcome message bytes from the task creator.

        Raises:
            RuntimeError: If the Welcome is invalid or joining fails.

        Example::

            # On the joining agent, after receiving *welcome* out-of-band:
            adapter.join_task("analysis-42", welcome_bytes)
        """
        channel = self._channel_name(task_id)
        self._manager.join(channel, welcome)
        logger.info("joined ACP task %s on channel %s", task_id, channel)

    # ------------------------------------------------------------------
    # Messaging
    # ------------------------------------------------------------------

    def send_message(self, task_id: str, payload: Dict) -> None:
        """Send an ACP message with the given payload.

        Builds a JSON object conforming to the ACP message shape, wraps
        it in a :class:`~skytale_sdk.envelope.Envelope` with ``Protocol.ACP``,
        and sends it over the MLS-encrypted channel.

        The message is assigned a random ``messageId`` (UUID4).  The
        ``agentId`` and ``taskId`` fields are populated automatically.

        Args:
            task_id: ACP task to send the message on.
            payload: Arbitrary dict payload for the ACP message.

        Raises:
            KeyError: If the task has not been created or joined.

        Example::

            adapter.send_message("analysis-42", {
                "status": "complete",
                "result": "3 anomalies detected",
            })
        """
        message_id = str(uuid.uuid4())
        acp_message: Dict = {
            "messageId": message_id,
            "taskId": task_id,
            "agentId": self._agent_id,
            "payload": payload,
        }

        raw = json.dumps(acp_message, separators=(",", ":")).encode("utf-8")

        envelope = Envelope(
            protocol=Protocol.ACP,
            content_type="application/json",
            payload=raw,
            metadata={"task_id": task_id},
        )

        channel = self._channel_name(task_id)
        self._manager.send_envelope(channel, envelope)
        logger.debug(
            "sent ACP message %s on task %s (%d bytes)",
            message_id,
            task_id,
            len(raw),
        )

    def receive_messages(
        self, task_id: str, timeout: float = 5.0
    ) -> List[Dict]:
        """Receive ACP messages from a task.

        Drains all buffered envelopes from the channel, filters for
        ``Protocol.ACP``, deserializes their JSON payloads, and returns
        them as plain dicts.

        Non-ACP envelopes (e.g. raw strings or A2A messages on a bridged
        channel) are silently skipped.  Envelopes whose payloads are not
        valid JSON are logged as warnings and skipped.

        Args:
            task_id: ACP task to read from.
            timeout: Maximum seconds to wait for messages.  ``0`` returns
                immediately.

        Returns:
            List of ACP message dicts.  Empty if no ACP messages arrive
            before the timeout expires.

        Raises:
            KeyError: If the task has not been created or joined.
            RuntimeError: If the background listener thread has died.

        Example::

            msgs = adapter.receive_messages("analysis-42", timeout=2.0)
            for msg in msgs:
                print(msg["payload"])
        """
        channel = self._channel_name(task_id)
        envelopes = self._manager.receive_envelopes(channel, timeout=timeout)

        messages: List[Dict] = []
        for env in envelopes:
            if env.protocol is not Protocol.ACP:
                continue
            try:
                msg = json.loads(env.payload)
            except (json.JSONDecodeError, UnicodeDecodeError) as exc:
                logger.warning(
                    "skipping ACP envelope with invalid JSON payload: %s", exc
                )
                continue
            messages.append(msg)

        logger.debug(
            "received %d ACP message(s) on task %s",
            len(messages),
            task_id,
        )
        return messages
