"""LMOS (Eclipse Large Model Operating System) adapter for Skytale channels.

Maps Eclipse LMOS WoT (Web of Things) concepts to Skytale's MLS-encrypted
channel infrastructure.  Each LMOS Thing ID becomes a Skytale channel at
``org/lmos/{thing_id}``, and LMOS action invocations are JSON-serialized,
wrapped in an :class:`~skytale_sdk.envelope.Envelope` tagged with
``Protocol.LMOS``, and transmitted through MLS-encrypted channels.

The adapter works with plain Python dicts; no external LMOS dependency
is required.

Usage::

    from skytale_sdk.channels import SkytaleChannelManager
    from skytale_sdk.integrations._lmos import SkytaleLMOSAdapter

    mgr = SkytaleChannelManager(identity=b"my-agent")
    adapter = SkytaleLMOSAdapter(mgr, thing_id="urn:lmos:agent:analyzer")

    adapter.create_channel("urn:lmos:agent:peer")
    adapter.invoke_action("urn:lmos:agent:peer", "analyze", {"dataset": "q4"})
    msgs = adapter.receive_messages("urn:lmos:agent:peer")
"""

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 SkytaleLMOSAdapter:
    """Maps LMOS WoT concepts to Skytale encrypted channels.

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

    Args:
        manager: A :class:`SkytaleChannelManager` that owns the underlying
            encrypted channels.
        thing_id: WoT Thing identifier for this agent.

    Example::

        from skytale_sdk.channels import SkytaleChannelManager
        from skytale_sdk.integrations._lmos import SkytaleLMOSAdapter

        mgr = SkytaleChannelManager(identity=b"lmos-agent")
        adapter = SkytaleLMOSAdapter(mgr, thing_id="urn:lmos:agent:analyzer")
        adapter.create_channel("urn:lmos:agent:peer")
        adapter.invoke_action("urn:lmos:agent:peer", "analyze", {"depth": 3})
    """

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

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

    def _channel_name(self, target_thing_id: str) -> str:
        """Convert a LMOS Thing ID to a Skytale channel name.

        LMOS channels live under the ``org/lmos/`` namespace.

        Args:
            target_thing_id: The target Thing identifier.

        Returns:
            A Skytale channel name in ``org/lmos/{thing_id}`` format.

        Example::

            >>> adapter._channel_name("analyzer-1")
            'org/lmos/analyzer-1'
        """
        return f"org/lmos/{target_thing_id}"

    # ------------------------------------------------------------------
    # Channel lifecycle
    # ------------------------------------------------------------------

    def create_channel(self, target_thing_id: str) -> None:
        """Create a channel to a target LMOS Thing.

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

        Args:
            target_thing_id: Thing ID of the target agent.

        Raises:
            RuntimeError: If channel creation fails.

        Example::

            adapter.create_channel("analyzer-1")
        """
        channel = self._channel_name(target_thing_id)
        self._manager.create(channel)
        logger.info(
            "created LMOS channel to %s on %s", target_thing_id, channel
        )

    def join_channel(self, target_thing_id: str, welcome: bytes) -> None:
        """Join an existing LMOS channel using an MLS Welcome message.

        Args:
            target_thing_id: Thing ID of the target agent.
            welcome: MLS Welcome message bytes.

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

        Example::

            adapter.join_channel("analyzer-1", welcome_bytes)
        """
        channel = self._channel_name(target_thing_id)
        self._manager.join(channel, welcome)
        logger.info(
            "joined LMOS channel to %s on %s", target_thing_id, channel
        )

    # ------------------------------------------------------------------
    # Actions
    # ------------------------------------------------------------------

    def invoke_action(
        self, target: str, action: str, input_data: Dict
    ) -> None:
        """Invoke a WoT action on a target Thing.

        Builds a LMOS ``invokeAction`` message, wraps it in an
        :class:`Envelope` with ``Protocol.LMOS``, and sends it.

        Args:
            target: Thing ID of the target agent.
            action: Action name to invoke.
            input_data: Input parameters for the action.

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

        Example::

            adapter.invoke_action("analyzer-1", "analyze", {"depth": 3})
        """
        message_id = str(uuid.uuid4())
        lmos_message: Dict = {
            "messageId": message_id,
            "type": "invokeAction",
            "sourceThingId": self._thing_id,
            "targetThingId": target,
            "action": action,
            "input": input_data,
        }

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

        envelope = Envelope(
            protocol=Protocol.LMOS,
            content_type="application/json",
            payload=raw,
            metadata={"thing_id": target, "action": action},
        )

        channel = self._channel_name(target)
        self._manager.send_envelope(channel, envelope)
        logger.debug(
            "invoked LMOS action %s on %s (%d bytes)",
            action,
            target,
            len(raw),
        )

    def send_action_status(
        self, target: str, action: str, status: str, output: Dict
    ) -> None:
        """Send an action status update to a target Thing.

        Args:
            target: Thing ID of the target agent.
            action: Action name this status is for.
            status: Status string (e.g. ``"completed"``, ``"failed"``).
            output: Output data for the action.

        Example::

            adapter.send_action_status(
                "requester-1", "analyze", "completed",
                {"result": "3 anomalies found"},
            )
        """
        message_id = str(uuid.uuid4())
        lmos_message: Dict = {
            "messageId": message_id,
            "type": "actionStatus",
            "sourceThingId": self._thing_id,
            "targetThingId": target,
            "action": action,
            "status": status,
            "output": output,
        }

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

        envelope = Envelope(
            protocol=Protocol.LMOS,
            content_type="application/json",
            payload=raw,
            metadata={"thing_id": target, "message_type": "actionStatus"},
        )

        channel = self._channel_name(target)
        self._manager.send_envelope(channel, envelope)
        logger.debug("sent LMOS actionStatus for %s to %s", action, target)

    def send_event(self, target: str, event_name: str, data: Dict) -> None:
        """Publish a WoT event to a target Thing.

        Args:
            target: Thing ID of the target agent.
            event_name: Name of the event.
            data: Event payload.

        Example::

            adapter.send_event("monitor-1", "anomaly_detected", {"count": 3})
        """
        message_id = str(uuid.uuid4())
        lmos_message: Dict = {
            "messageId": message_id,
            "type": "event",
            "sourceThingId": self._thing_id,
            "targetThingId": target,
            "event": event_name,
            "data": data,
        }

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

        envelope = Envelope(
            protocol=Protocol.LMOS,
            content_type="application/json",
            payload=raw,
            metadata={"thing_id": target, "message_type": "event"},
        )

        channel = self._channel_name(target)
        self._manager.send_envelope(channel, envelope)
        logger.debug("sent LMOS event %s to %s", event_name, target)

    # ------------------------------------------------------------------
    # Receive
    # ------------------------------------------------------------------

    def receive_messages(
        self, target: str, timeout: float = 5.0
    ) -> List[Dict]:
        """Receive LMOS messages from a target Thing channel.

        Drains all buffered envelopes, filters for ``Protocol.LMOS``,
        and returns deserialized message dicts.

        Args:
            target: Thing ID to read from.
            timeout: Maximum seconds to wait for messages.

        Returns:
            List of LMOS message dicts.

        Example::

            msgs = adapter.receive_messages("analyzer-1", timeout=2.0)
            for msg in msgs:
                print(msg["type"], msg.get("action"))
        """
        channel = self._channel_name(target)
        envelopes = self._manager.receive_envelopes(channel, timeout=timeout)

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

        logger.debug(
            "received %d LMOS message(s) from %s",
            len(messages),
            target,
        )
        return messages
