"""Message data class and fluent MessageBuilder.

Analogous to ``db_core.query.builder`` — ``Message`` is the data container
(like ``Query``) and ``MessageBuilder`` provides the fluent chain (like
``QueryBuilder``).
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from ..clients.base import BrokerClient
    from .result import MessageResult, Subscription


# ---------------------------------------------------------------------------
# Message — the built result of the builder
# ---------------------------------------------------------------------------


@dataclass
class Message:
    """A message ready for publishing or subscription.

    This is the data object that the ``MessageBuilder`` accumulates and
    that the backend client consumes.
    """

    topic: str
    key: Optional[str] = None
    payload: Optional[Dict[str, Any]] = None
    headers: Dict[str, str] = field(default_factory=dict)
    partition: Optional[int] = None
    timestamp_ms: Optional[int] = None

    def to_dict(self) -> Dict[str, Any]:
        """Serialise the message to a plain dictionary (for debugging)."""
        return {
            "topic": self.topic,
            "key": self.key,
            "payload": self.payload,
            "headers": self.headers,
            "partition": self.partition,
            "timestamp_ms": self.timestamp_ms,
        }

    def debug(self) -> str:
        """Human-readable debug representation (like ``debug_query()``)."""
        return json.dumps(self.to_dict(), indent=2, default=str)


# ---------------------------------------------------------------------------
# MessageBuilder — fluent builder
# ---------------------------------------------------------------------------


class MessageBuilder:
    """Fluent builder for constructing and executing message operations.

    Created via ``client.topic("name")``.  Chain message-content or
    consumer-configuration methods, then call a terminal method
    (``.publish()``, ``.subscribe()``, ``.listen()``).

    The builder is **reusable** — after a terminal call the internal state
    resets so the same builder instance can be used for the next operation
    on the same topic.
    """

    def __init__(self, client: "BrokerClient", topic: str):
        self._client = client
        self._topic = topic
        self._key: Optional[str] = None
        self._payload: Optional[Dict[str, Any]] = None
        self._headers: Dict[str, str] = {}
        self._partition: Optional[int] = None

        # Consumer configuration (overrides KafkaConfig defaults)
        self._consumer_group_id: Optional[str] = None
        self._consumer_client_id: Optional[str] = None
        self._auto_offset_reset: Optional[str] = None
        self._enable_auto_commit: Optional[bool] = None
        self._auto_commit_interval_ms: Optional[int] = None
        self._max_poll_records: Optional[int] = None
        self._session_timeout_ms: Optional[int] = None

    # ------------------------------------------------------------------
    # Message content (for publish)
    # ------------------------------------------------------------------

    def key(self, key: str) -> "MessageBuilder":
        """Set the message key (determines partition routing)."""
        self._key = key
        return self

    def payload(self, data: Dict[str, Any]) -> "MessageBuilder":
        """Set the message body as a JSON-serialisable dictionary."""
        self._payload = data
        return self

    def headers(self, headers: Dict[str, str]) -> "MessageBuilder":
        """Set metadata headers (merged with any previously set)."""
        self._headers.update(headers)
        return self

    def header(self, key: str, value: str) -> "MessageBuilder":
        """Add a single metadata header."""
        self._headers[key] = value
        return self

    def partition(self, n: int) -> "MessageBuilder":
        """Target a specific partition (overrides key-based routing)."""
        self._partition = n
        return self

    # ------------------------------------------------------------------
    # Consumer configuration (for subscribe / listen)
    # ------------------------------------------------------------------

    def group_id(self, gid: str) -> "MessageBuilder":
        """Consumer group identifier (required for multi-consumer setups)."""
        self._consumer_group_id = gid
        return self

    def client_id(self, cid: str) -> "MessageBuilder":
        """Per-consumer client ID override."""
        self._consumer_client_id = cid
        return self

    def auto_offset_reset(self, policy: str) -> "MessageBuilder":
        """Where to start when no committed offset exists.

        Args:
            policy: ``"earliest"`` or ``"latest"``.
        """
        if policy not in ("earliest", "latest"):
            raise ValueError(f"auto_offset_reset must be 'earliest' or 'latest', got {policy!r}")
        self._auto_offset_reset = policy
        return self

    def enable_auto_commit(self, enable: bool) -> "MessageBuilder":
        """Enable or disable automatic offset commits."""
        self._enable_auto_commit = enable
        return self

    def auto_commit_interval_ms(self, ms: int) -> "MessageBuilder":
        """Interval in milliseconds between automatic offset commits."""
        self._auto_commit_interval_ms = ms
        return self

    def max_poll_records(self, n: int) -> "MessageBuilder":
        """Maximum number of records returned in a single poll."""
        self._max_poll_records = n
        return self

    def session_timeout_ms(self, ms: int) -> "MessageBuilder":
        """Consumer session timeout in milliseconds."""
        self._session_timeout_ms = ms
        return self

    def from_beginning(self) -> "MessageBuilder":
        """Sugar for ``.auto_offset_reset("earliest")``."""
        self._auto_offset_reset = "earliest"
        return self

    # ------------------------------------------------------------------
    # Introspection
    # ------------------------------------------------------------------

    def to_dict(self) -> Dict[str, Any]:
        """Return the current built message as a dictionary."""
        return self._build_message().to_dict()

    def debug(self) -> str:
        """Human-readable debug representation of the built message."""
        return self._build_message().debug()

    # ------------------------------------------------------------------
    # Terminal: Publish
    # ------------------------------------------------------------------

    async def publish(self) -> "MessageResult":
        """Publish the message and return the broker acknowledgment."""
        msg = self._build_message()
        self._reset()
        return await self._client._publish(msg)

    async def emit(self) -> "MessageResult":
        """Alias for :meth:`publish`."""
        return await self.publish()

    # ------------------------------------------------------------------
    # Terminal: Subscribe
    # ------------------------------------------------------------------

    async def subscribe(self) -> "Subscription":
        """Start a consumer and return a :class:`Subscription` async iterator.

        Use as an async context manager::

            async with client.topic("orders").subscribe() as sub:
                async for msg in sub:
                    print(msg.payload)
        """
        msg = self._build_message()
        sub = await self._client._subscribe(msg, self)
        self._reset()
        return sub

    async def listen(self, callback: Callable) -> "Subscription":
        """Start a callback-based consumer.

        The callback receives each :class:`ReceivedMessage`::

            async def handle(msg):
                await msg.ack()

            sub = await client.topic("orders").listen(handle)
        """
        msg = self._build_message()
        sub = await self._client._subscribe(msg, self)
        # Wrap the subscription to invoke the callback per message
        sub._start_callback(callback)
        self._reset()
        return sub

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

    def _build_message(self) -> Message:
        """Construct the :class:`Message` from accumulated builder state."""
        return Message(
            topic=self._topic,
            key=self._key,
            payload=self._payload,
            headers=dict(self._headers),
            partition=self._partition,
        )

    def _reset(self) -> None:
        """Reset per-operation state so the builder is reusable."""
        self._key = None
        self._payload = None
        self._headers = {}
        self._partition = None
        # Consumer settings are intentionally NOT reset — they are
        # "sticky" per builder instance so that repeated subscribe()
        # calls on the same topic reuse the same consumer config.
