"""Abstract base class for message channels."""

import json
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Any, Awaitable, Callable, Optional


@dataclass
class ChannelHealth:
    ok: bool
    message: str = ""


@dataclass
class SendResult:
    ok: bool
    question_msg_id: Optional[str] = None
    raw_response: Optional[dict[str, Any]] = None
    error: Optional[str] = None


@dataclass
class Reply:
    question_msg_id: str
    content: Any
    sender_name: Optional[str] = None
    msg_time: Optional[datetime] = None
    raw_message: Optional[dict[str, Any]] = None


class ExchangeStatus(str, Enum):
    """Outcome categories for one complete channel exchange."""

    SUCCESS = "success"
    SEND_FAILED = "send_failed"
    REPLY_TIMEOUT = "reply_timeout"
    POLL_FAILED = "poll_failed"


class ChannelTransportError(Exception):
    """Expected operational transport failure raised by a channel adapter.

    Configuration and programming errors use their original exception types
    and deliberately cross the exchange interface for diagnosis.
    """


def normalize_reply_text(content: Any) -> str:
    """Convert a channel reply payload into stable plain text.

    Adapters may return a plain string, a Tutu-style ``msgBody`` object, or a
    provider-specific mapping.  The exchange interface exposes only text so
    callers do not need to learn each provider's response shape.
    """
    if content is None:
        return ""
    if isinstance(content, str):
        return content
    if isinstance(content, dict):
        body = content.get("msgBody")
        if isinstance(body, dict):
            for key in ("content", "text", "message"):
                value = body.get(key)
                if isinstance(value, str) and value:
                    return value
        elif isinstance(body, str) and body:
            return body

        for key in ("content", "text", "message"):
            value = content.get(key)
            if isinstance(value, str) and value:
                return value
        return json.dumps(content, ensure_ascii=False)
    return str(content)


@dataclass(frozen=True)
class ExchangeOutcome:
    """Result of a complete send-and-reply exchange.

    ``diagnostic`` is intentionally opaque to callers.  Adapters may retain
    provider-specific data there without expanding the public interface.
    """

    status: ExchangeStatus
    correlation_id: Optional[str] = None
    reply_text: Optional[str] = None
    latency_ms: Optional[int] = None
    reason: Optional[str] = None
    diagnostic: Any = None

    def __post_init__(self) -> None:
        if self.status is ExchangeStatus.SUCCESS:
            if not self.correlation_id:
                raise ValueError("a successful exchange requires a correlation_id")
            if self.reply_text is None:
                raise ValueError("a successful exchange requires reply_text")
            if self.latency_ms is None or self.latency_ms < 0:
                raise ValueError("a successful exchange requires non-negative latency_ms")

    @property
    def ok(self) -> bool:
        return self.status is ExchangeStatus.SUCCESS

    @property
    def expected_failure(self) -> bool:
        return self.status in {
            ExchangeStatus.SEND_FAILED,
            ExchangeStatus.REPLY_TIMEOUT,
            ExchangeStatus.POLL_FAILED,
        }

    @classmethod
    def succeeded(
        cls,
        *,
        correlation_id: str,
        reply: Any,
        latency_ms: int,
        diagnostic: Any = None,
    ) -> "ExchangeOutcome":
        return cls(
            status=ExchangeStatus.SUCCESS,
            correlation_id=correlation_id,
            reply_text=normalize_reply_text(reply),
            latency_ms=latency_ms,
            diagnostic=diagnostic,
        )

    @classmethod
    def send_failed(cls, reason: str, *, diagnostic: Any = None) -> "ExchangeOutcome":
        return cls(status=ExchangeStatus.SEND_FAILED, reason=reason, diagnostic=diagnostic)

    @classmethod
    def reply_timeout(
        cls,
        *,
        correlation_id: Optional[str] = None,
        latency_ms: Optional[int] = None,
        diagnostic: Any = None,
    ) -> "ExchangeOutcome":
        return cls(
            status=ExchangeStatus.REPLY_TIMEOUT,
            correlation_id=correlation_id,
            latency_ms=latency_ms,
            reason="reply timeout",
            diagnostic=diagnostic,
        )

    @classmethod
    def poll_failed(
        cls,
        reason: str,
        *,
        correlation_id: Optional[str] = None,
        latency_ms: Optional[int] = None,
        diagnostic: Any = None,
    ) -> "ExchangeOutcome":
        return cls(
            status=ExchangeStatus.POLL_FAILED,
            correlation_id=correlation_id,
            latency_ms=latency_ms,
            reason=reason,
            diagnostic=diagnostic,
        )


class EvalChannel(ABC):
    """Abstract message channel used to interact with an evaluation target.

    All methods are async so push-based channels (WebSocket, SSE) and
    poll-based channels (REST) share the same interface.
    """

    @abstractmethod
    async def health_check(self) -> ChannelHealth:
        """Verify the channel can reach the target."""
        ...

    async def exchange(
        self,
        content: str,
        *,
        timeout: float = 30.0,
        poll_interval: float = 1.0,
        on_sent: Optional[Callable[[SendResult], Awaitable[None]]] = None,
        **kwargs: Any,
    ) -> ExchangeOutcome:
        """Complete one send-and-reply exchange through this channel.

        ``on_sent`` runs after the adapter confirms a successful send and
        before reply polling begins.  A hook failure is deliberately allowed
        to propagate so callers cannot observe a reply without a durable sent
        record.
        """
        started_at = time.monotonic()
        try:
            send_result = await self._send(content, **kwargs)
        except ChannelTransportError as exc:
            return ExchangeOutcome.send_failed(str(exc))
        if not send_result.ok:
            return ExchangeOutcome.send_failed(send_result.error or "channel send failed")

        correlation_id = send_result.question_msg_id
        if not correlation_id:
            raise ValueError("a successful send must provide question_msg_id")

        if on_sent is not None:
            await on_sent(send_result)

        try:
            reply = await self._poll_reply(
                correlation_id,
                timeout=timeout,
                poll_interval=poll_interval,
            )
        except ChannelTransportError as exc:
            return ExchangeOutcome.poll_failed(
                str(exc),
                correlation_id=correlation_id,
                latency_ms=_elapsed_ms(started_at),
            )

        latency_ms = _elapsed_ms(started_at)
        if reply is None:
            return ExchangeOutcome.reply_timeout(
                correlation_id=correlation_id,
                latency_ms=latency_ms,
            )

        return ExchangeOutcome.succeeded(
            correlation_id=correlation_id,
            reply=reply.content,
            latency_ms=latency_ms,
            diagnostic={"send": send_result.raw_response, "reply": reply.raw_message},
        )

    @abstractmethod
    async def _send(self, content: str, **kwargs: Any) -> SendResult:
        """Transport primitive used by :meth:`exchange`."""
        ...

    @abstractmethod
    async def _poll_reply(
        self,
        question_msg_id: str,
        timeout: float = 30.0,
        poll_interval: float = 1.0,
    ) -> Optional[Reply]:
        """Transport primitive used by :meth:`exchange`."""
        ...


def _elapsed_ms(started_at: float) -> int:
    return max(0, int((time.monotonic() - started_at) * 1000))
