"""
RCAN Message Envelope.

Defines the standard JSON message format for RCAN protocol communication.
Messages are plain JSON (no protobuf) -- readable with ``curl``, zero deps.

Message types follow the RCAN spec::

    DISCOVER      -- mDNS / peer discovery
    STATUS        -- Telemetry / state reporting
    COMMAND       -- Motor, config, or action command
    STREAM        -- Continuous sensor data
    EVENT         -- Asynchronous notifications
    SAFETY        -- STOP / ESTOP / RESUME safety events (highest priority)
    HANDOFF       -- Transfer control between principals
    ACK           -- Acknowledgement of a prior message
    ERROR         -- Error response
    AUTHORIZE     -- Out-of-band authorization for HiTL gate (v1.2)
    PENDING_AUTH  -- Notification that HiTL gate is awaiting authorization (v1.2)
    INVOKE             -- Trigger a named skill/behavior on the robot runtime (v1.3 §19)
    INVOKE_RESULT      -- Result of an INVOKE invocation (v1.3 §19)
    INVOKE_CANCEL      -- Cancel an in-flight INVOKE by invoke_id (v1.3 §19)
    REGISTRY_REGISTER        -- Register robot with RRF (v1.3 §21)
    REGISTRY_RESOLVE         -- Resolve RRN to RURI/metadata (v1.3 §21)
    REGISTRY_REGISTER_RESULT -- Result of REGISTRY_REGISTER (v1.3 §21)
    REGISTRY_RESOLVE_RESULT  -- Result of REGISTRY_RESOLVE (v1.3 §21)

Each message carries a priority (LOW, NORMAL, HIGH, SAFETY) that determines
queue ordering.  SAFETY priority messages skip the queue entirely
(Safety Invariant 6).
"""

from __future__ import annotations

import logging
import time
import uuid
from dataclasses import asdict, dataclass, field
from enum import IntEnum
from typing import Any, Optional

log = logging.getLogger(__name__)

# RCAN spec version implemented by this module
RCAN_SPEC_VERSION = "2.2"


class MessageType(IntEnum):
    """RCAN message types."""

    DISCOVER = 1
    STATUS = 2
    COMMAND = 3
    STREAM = 4
    EVENT = 5
    SAFETY = 6  # RCAN §6: STOP / ESTOP / RESUME — bypasses all queues
    ACK = 7
    ERROR = 8
    AUTHORIZE = 9  # Out-of-band authorization for HiTL gate (RCAN v1.2)
    PENDING_AUTH = 10  # Notification that HiTL gate is awaiting authorization (RCAN v1.2)
    INVOKE = 11  # Trigger a named skill/behavior on the robot runtime (RCAN v1.3 §19)
    INVOKE_RESULT = 12  # Result of an INVOKE invocation (RCAN v1.3 §19)
    REGISTRY_REGISTER = 13  # §21 — register robot with RRF
    REGISTRY_RESOLVE = 14  # §21 — resolve RRN to RURI/metadata
    INVOKE_CANCEL = 15  # Cancel an in-flight INVOKE by invoke_id (RCAN v1.3 §19)
    REGISTRY_REGISTER_RESULT = (
        16  # §21 — result of REGISTRY_REGISTER (success/failure + assigned RRN)
    )
    REGISTRY_RESOLVE_RESULT = 17  # §21 — result of REGISTRY_RESOLVE (RURI + metadata or error)
    TRANSPARENCY = 18  # EU AI Act Art. 13 transparency disclosure
    HANDOFF = 19  # Transfer control between principals
    CONSENT_REQUEST = 20  # R2RAM §5: request cross-owner robot-to-robot authorization
    CONSENT_GRANT = 21  # R2RAM §5: grant cross-owner authorization with scopes
    CONSENT_DENY = 22  # R2RAM §5: deny cross-owner authorization request
    FLEET_COMMAND = 23  # Fleet-level command from orchestrator (v2.1)
    SUBSCRIBE = 24  # Subscribe to a robot event stream
    UNSUBSCRIBE = 25  # Unsubscribe from a robot event stream
    FAULT_REPORT = 26  # Fault report from robot to registry
    KEY_ROTATION = 27  # Ed25519 key rotation notification
    COMMAND_COMMIT = 28  # Commit a pending command (HiTL confirmation)
    SENSOR_DATA = 29  # Sensor data push from robot
    TRAINING_CONSENT_REQUEST = 30  # Request consent for training data collection
    TRAINING_CONSENT_GRANT = 31  # Grant training data consent
    TRAINING_CONSENT_DENY = 32  # Deny training data consent
    CONTRIBUTE_REQUEST = 33  # Idle compute contribution offer (v1.7)
    CONTRIBUTE_RESULT = 34  # Contribution task result
    CONTRIBUTE_CANCEL = 35  # Cancel an active contribution
    TRAINING_DATA = 36  # Training data payload
    COMPETITION_ENTER = 37  # Competition protocol — enter event (v1.10)
    COMPETITION_SCORE = 38  # Competition protocol — score report (v1.10)
    SEASON_STANDING = 39  # Competition protocol — season standings (v1.10)
    PERSONAL_RESEARCH_RESULT = 40  # Research result from personal compute (v1.10)
    AUTHORITY_ACCESS = 41  # EU AI Act §16(j) authority audit request (v2.1)
    AUTHORITY_RESPONSE = 42  # Response to authority audit request (v2.1)
    FIRMWARE_ATTESTATION = 43  # Firmware manifest attestation broadcast (v2.1)
    SBOM_UPDATE = 44  # SBOM update notification (v2.1)


@dataclass
class DelegationHop:
    """A single hop in a delegation chain."""

    robot_rrn: str
    scope: str
    issued_at: str
    expires_at: str
    sig: str = ""


@dataclass
class MediaChunk:
    """A media chunk attached to an RCAN message."""

    chunk_id: str
    mime_type: str
    size_bytes: int
    hash_sha256: str
    data: str = ""
    ref_url: str = ""

    def verify_hash(self) -> None:
        """Verify the SHA-256 hash of the data field.

        Raises:
            ValueError: If the hash does not match.
        """
        import hashlib

        actual = "sha256:" + hashlib.sha256(self.data.encode()).hexdigest()
        if actual != self.hash_sha256:
            raise ValueError(f"MediaChunk hash mismatch: expected {self.hash_sha256}, got {actual}")


class Priority(IntEnum):
    """Message priority levels.  SAFETY skips the normal queue."""

    LOW = 0
    NORMAL = 1
    HIGH = 2
    SAFETY = 3


@dataclass
class RCANMessage:
    """Standard RCAN protocol message envelope.

    Attributes:
        id:          Unique message identifier (UUID).
        type:        Message type enum value.
        priority:    Priority level.
        source:      Source RURI string.
        target:      Target RURI string (may contain wildcards).
        payload:     Arbitrary JSON-serialisable data.
        timestamp:   Unix timestamp (seconds since epoch).
        ttl:         Time-to-live in seconds (0 = no expiry).
        reply_to:    ID of the message this is a reply to.
        scope:       Required RBAC scopes for this message.
        version:     RCAN protocol version.
    """

    type: int
    source: str
    target: str
    payload: dict[str, Any] = field(default_factory=dict)
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    timestamp: float = field(default_factory=time.time)
    priority: int = field(default=Priority.NORMAL)
    ttl: int = field(default=0)
    reply_to: Optional[str] = field(default=None)
    scope: list[str] = field(default_factory=list)
    version: str = field(default="1.0.0")
    rcan_version: str = field(default_factory=lambda: RCAN_SPEC_VERSION)  # v1.5 §3.5

    # v2.2 envelope fields
    firmware_hash: str = ""
    attestation_ref: str = ""
    pq_sig: str = ""
    pq_alg: str = "ml-dsa-65"
    delegation_chain: list = field(default_factory=list)
    media_chunks: list = field(default_factory=list)

    # ------------------------------------------------------------------
    # Factory methods
    # ------------------------------------------------------------------
    @classmethod
    def command(
        cls,
        source: str,
        target: str,
        payload: dict[str, Any],
        priority: int = Priority.NORMAL,
        scope: Optional[list[str]] = None,
    ) -> RCANMessage:
        """Create a COMMAND message."""
        return cls(
            type=MessageType.COMMAND,
            source=source,
            target=target,
            payload=payload,
            priority=priority,
            scope=scope or ["control"],
        )

    @classmethod
    def status(
        cls,
        source: str,
        target: str,
        payload: dict[str, Any],
    ) -> RCANMessage:
        """Create a STATUS message."""
        return cls(
            type=MessageType.STATUS,
            source=source,
            target=target,
            payload=payload,
            scope=["status"],
        )

    @classmethod
    def ack(
        cls,
        source: str,
        target: str,
        reply_to: str,
        payload: Optional[dict[str, Any]] = None,
    ) -> RCANMessage:
        """Create an ACK for a prior message."""
        return cls(
            type=MessageType.ACK,
            source=source,
            target=target,
            reply_to=reply_to,
            payload=payload or {},
        )

    @classmethod
    def error(
        cls,
        source: str,
        target: str,
        code: str,
        detail: str,
        reply_to: Optional[str] = None,
    ) -> RCANMessage:
        """Create an ERROR message."""
        return cls(
            type=MessageType.ERROR,
            source=source,
            target=target,
            reply_to=reply_to,
            payload={"code": code, "detail": detail},
        )

    @classmethod
    def authorize(
        cls,
        source: str,
        target: str,
        ref_message_id: str,
        principal: str,
        decision: str,
        **kwargs: Any,
    ) -> RCANMessage:
        """Create an AUTHORIZE message for out-of-band HiTL gate authorization.

        Args:
            source:         RURI of the authorizing principal.
            target:         RURI of the robot or gateway receiving the decision.
            ref_message_id: ID of the PENDING_AUTH message being responded to.
            principal:      Identity of the authorizing principal (e.g. user ID).
            decision:       Must be ``'approve'`` or ``'deny'``.
            **kwargs:       Additional fields forwarded to the message payload.

        Raises:
            ValueError: If *decision* is not ``'approve'`` or ``'deny'``.
        """
        if decision not in ("approve", "deny"):
            raise ValueError(f"AUTHORIZE decision must be 'approve' or 'deny', got {decision!r}")
        payload: dict[str, Any] = {
            "ref_message_id": ref_message_id,
            "principal": principal,
            "decision": decision,
        }
        payload.update(kwargs)
        return cls(
            type=MessageType.AUTHORIZE,
            source=source,
            target=target,
            payload=payload,
            priority=Priority.HIGH,
            scope=["hitl", "control"],
        )

    @classmethod
    def pending_auth(
        cls,
        source: str,
        target: str,
        pending_id: str,
        action_type: str,
        description: str,
        timeout_remaining_ms: int,
        **kwargs: Any,
    ) -> RCANMessage:
        """Create a PENDING_AUTH notification message.

        Sent by the HiTL gate to notify subscribers that an action is
        awaiting out-of-band authorization before it can be dispatched.

        Args:
            source:              RURI of the robot / gateway.
            target:              RURI of the principal(s) who can authorize.
            pending_id:          Unique ID for this pending authorization request.
            action_type:         The action type awaiting authorization.
            description:         Human-readable description of the action.
            timeout_remaining_ms: Milliseconds until the gate times out.
            **kwargs:            Additional fields forwarded to the message payload.
        """
        payload: dict[str, Any] = {
            "pending_id": pending_id,
            "action_type": action_type,
            "description": description,
            "timeout_remaining_ms": timeout_remaining_ms,
        }
        payload.update(kwargs)
        return cls(
            type=MessageType.PENDING_AUTH,
            source=source,
            target=target,
            payload=payload,
            priority=Priority.HIGH,
            scope=["hitl", "status"],
        )

    # ------------------------------------------------------------------
    # Serialisation
    # ------------------------------------------------------------------
    def to_dict(self) -> dict[str, Any]:
        """Serialise to a plain dict (JSON-ready).

        v1.5: includes rcan_version in outgoing messages (GAP-12).
        v2.2: includes envelope fields (firmware_hash, attestation_ref, pq_sig,
              pq_alg, delegation_chain, media_chunks).
        """
        d = asdict(self)
        # Convert enum ints to their names for readability
        d["type_name"] = MessageType(self.type).name
        d["priority_name"] = Priority(self.priority).name
        # Ensure rcan_version is always present in outgoing messages
        d.setdefault("rcan_version", RCAN_SPEC_VERSION)
        # v2.2 envelope fields are already included via asdict()
        d.setdefault("firmware_hash", self.firmware_hash)
        d.setdefault("attestation_ref", self.attestation_ref)
        d.setdefault("pq_sig", self.pq_sig)
        d.setdefault("pq_alg", self.pq_alg)
        d.setdefault("delegation_chain", self.delegation_chain)
        d.setdefault("media_chunks", self.media_chunks)
        return d

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> RCANMessage:
        """Deserialise from a dict.

        Accepts both integer type/priority values and string names.

        v1.5: logs a warning (not error) when receiving messages with a
        different rcan_version (GAP-12 — forward/backward compat).
        """
        d = dict(data)

        # Remove display-only fields
        d.pop("type_name", None)
        d.pop("priority_name", None)

        # v1.5 version negotiation — warn on mismatch, don't reject
        incoming_version = d.get("rcan_version")
        if incoming_version and incoming_version != RCAN_SPEC_VERSION:
            try:
                inc_parts = incoming_version.split(".")
                our_parts = RCAN_SPEC_VERSION.split(".")
                inc_major = int(inc_parts[0])
                our_major = int(our_parts[0])
                if inc_major != our_major:
                    log.warning(
                        "Received RCAN message with incompatible MAJOR version "
                        "%s (ours: %s) — proceeding with caution",
                        incoming_version,
                        RCAN_SPEC_VERSION,
                    )
                else:
                    log.warning(
                        "Received RCAN message with version %s (ours: %s) — "
                        "unknown fields will be ignored",
                        incoming_version,
                        RCAN_SPEC_VERSION,
                    )
            except (ValueError, IndexError):
                log.warning(
                    "Received RCAN message with unparseable rcan_version=%r",
                    incoming_version,
                )

        # Coerce type from name if needed
        if isinstance(d.get("type"), str):
            d["type"] = MessageType[d["type"].upper()]
        # Coerce priority from name if needed
        if isinstance(d.get("priority"), str):
            d["priority"] = Priority[d["priority"].upper()]

        # Populate v2.2 envelope fields
        d.setdefault("firmware_hash", "")
        d.setdefault("attestation_ref", "")
        d.setdefault("pq_sig", "")
        d.setdefault("pq_alg", "ml-dsa-65")
        d.setdefault("delegation_chain", [])
        d.setdefault("media_chunks", [])

        # Validate delegation chain depth (RCAN v2.2 §7)
        if len(d["delegation_chain"]) > 3:
            raise ValueError("RCAN: delegation chain max depth is 3")

        # Strip unknown fields not in the dataclass (forward-compat)
        import dataclasses as _dc

        known = {f.name for f in _dc.fields(cls)}
        d = {k: v for k, v in d.items() if k in known}

        return cls(**d)

    # ------------------------------------------------------------------
    # Validation
    # ------------------------------------------------------------------
    def is_expired(self) -> bool:
        """Check if the message TTL has been exceeded."""
        if self.ttl <= 0:
            return False
        return (time.time() - self.timestamp) > self.ttl

    @property
    def is_safety(self) -> bool:
        """Return True if this is a SAFETY-priority message."""
        return self.priority == Priority.SAFETY
