"""Route and publish the user-visible lifecycle of an agent turn."""

from __future__ import annotations

import dataclasses
import time
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, cast

from nanobot.bus.events import InboundMessage, OutboundMessage
from nanobot.bus.outbound_events import (
    RetryWaitEvent,
    StreamDeltaEvent,
    StreamedResponseEvent,
    StreamEndEvent,
    outbound_message_for_event,
)
from nanobot.bus.progress import build_bus_progress_callback
from nanobot.bus.queue import MessageBus
from nanobot.bus.runtime_events import RuntimeEventBus, RuntimeEventPublisher
from nanobot.providers.base import LLMUsage

if TYPE_CHECKING:
    from nanobot.utils.llm_runtime import LLMRuntime


@dataclass(frozen=True)
class TurnRoute:
    """Turn delivery destination and lifecycle policy, separate from execution input."""

    channel: str
    chat_id: str
    metadata: dict[str, Any] = field(default_factory=dict)
    publish_lifecycle: bool = False


TurnRoutePolicy = Callable[[InboundMessage, str, TurnRoute], TurnRoute]
ProgressCallback = Callable[..., Awaitable[None]]
StreamCallback = Callable[[str], Awaitable[None]]
StreamEndCallback = Callable[..., Awaitable[None]]
RetryWaitCallback = Callable[[str], Awaitable[None]]


class TurnDeliveryFactory:
    """Create per-turn delivery objects from an optional edge-owned route policy."""

    def __init__(
        self,
        bus: MessageBus,
        runtime_events: RuntimeEventBus,
        route_policy: TurnRoutePolicy | None = None,
    ) -> None:
        self.bus = bus
        self.runtime_events = runtime_events
        self.runtime_event_publisher = RuntimeEventPublisher(runtime_events)
        self.route_policy = route_policy

    def create(
        self,
        msg: InboundMessage,
        session_key: str,
        *,
        enable_stream: bool = False,
    ) -> TurnDelivery:
        route = self._default_route(msg, session_key)
        if self.route_policy is not None:
            route = self.route_policy(msg, session_key, route)
            if not isinstance(cast(object, route), TurnRoute):
                raise TypeError("turn route policy must return TurnRoute")
        return TurnDelivery(
            bus=self.bus,
            runtime_event_publisher=self.runtime_event_publisher,
            input_message=msg,
            session_key=session_key,
            route=route,
            enable_stream=enable_stream,
        )

    def unrouted(self, msg: InboundMessage, session_key: str) -> TurnDelivery:
        """Create a lifecycle fallback without invoking edge routing policy."""
        return TurnDelivery(
            bus=self.bus,
            runtime_event_publisher=self.runtime_event_publisher,
            input_message=msg,
            session_key=session_key,
            route=TurnRoute(
                channel=msg.channel,
                chat_id=msg.chat_id,
                metadata=dict(msg.metadata or {}),
            ),
        )

    @staticmethod
    def _default_route(msg: InboundMessage, session_key: str) -> TurnRoute:
        if msg.channel != "system":
            return TurnRoute(
                channel=msg.channel,
                chat_id=msg.chat_id,
                metadata=dict(msg.metadata or {}),
                publish_lifecycle=True,
            )

        channel, chat_id = (
            msg.chat_id.split(":", 1) if ":" in msg.chat_id else ("cli", msg.chat_id)
        )
        metadata: dict[str, Any] = {}
        if (
            channel == "slack"
            and session_key.startswith("slack:")
            and session_key.count(":") >= 2
        ):
            metadata["slack"] = {"thread_ts": session_key.split(":", 2)[2]}
        if origin_message_id := msg.metadata.get("origin_message_id"):
            metadata["origin_message_id"] = origin_message_id
        return TurnRoute(channel=channel, chat_id=chat_id, metadata=metadata)


@dataclass
class TurnDelivery:
    """Own routing, callbacks, and lifecycle publication for one turn."""

    bus: MessageBus
    runtime_event_publisher: RuntimeEventPublisher
    input_message: InboundMessage
    session_key: str
    route: TurnRoute
    enable_stream: bool = False
    delivery_message: InboundMessage = field(init=False)
    lifecycle_message: InboundMessage = field(init=False)
    _stream_base_id: str | None = field(init=False, default=None)
    _stream_segment: int = field(init=False, default=0)
    _stream_open: bool = field(init=False, default=False)

    def __post_init__(self) -> None:
        self.delivery_message = dataclasses.replace(
            self.input_message,
            channel=self.route.channel,
            chat_id=self.route.chat_id,
            metadata=dict(self.route.metadata),
        )
        self.lifecycle_message = (
            self.delivery_message if self.route.publish_lifecycle else self.input_message
        )
        if self.enable_stream and self.delivery_message.metadata.get("_wants_stream"):
            self._stream_base_id = f"{self.session_key}:{time.time_ns()}"

    @property
    def on_stream(self) -> StreamCallback | None:
        return self._publish_stream if self._stream_base_id is not None else None

    @property
    def on_stream_end(self) -> StreamEndCallback | None:
        return self._publish_stream_end if self._stream_base_id is not None else None

    def progress_callback(self) -> ProgressCallback | None:
        if not self.route.publish_lifecycle:
            return None
        return build_bus_progress_callback(self.bus, self.delivery_message)

    def retry_wait_callback(self) -> RetryWaitCallback | None:
        if not self.route.publish_lifecycle:
            return None

        async def _on_retry_wait(content: str) -> None:
            await self.bus.publish_outbound(
                outbound_message_for_event(
                    channel=self.delivery_message.channel,
                    chat_id=self.delivery_message.chat_id,
                    event=RetryWaitEvent(content=content),
                    metadata=self.delivery_message.metadata,
                )
            )

        return _on_retry_wait

    async def started(self) -> None:
        if self.route.publish_lifecycle:
            await self.runtime_event_publisher.session_turn_started(
                self.delivery_message,
                self.session_key,
            )

    async def running(self, *, started_at: float) -> None:
        if self.route.publish_lifecycle:
            await self.runtime_event_publisher.run_status_changed(
                self.delivery_message,
                self.session_key,
                "running",
                started_at=started_at,
            )

    async def runtime_admitted(self, runtime: LLMRuntime) -> None:
        """Record the immutable runtime and expose it at the lifecycle seam."""
        if self.route.publish_lifecycle:
            await self.runtime_event_publisher.turn_runtime_admitted(
                self.delivery_message,
                self.session_key,
                runtime,
            )
            return
        self.runtime_event_publisher.record_turn_runtime(self.session_key, runtime)

    def record_latency(self, latency_ms: int | None) -> None:
        self.runtime_event_publisher.record_turn_latency(self.session_key, latency_ms)

    def record_usage(self, usage: LLMUsage | None) -> None:
        self.runtime_event_publisher.record_turn_usage(self.session_key, usage)

    def background_response(
        self,
        content: str | None,
        *,
        stop_reason: str,
        streamed: bool,
        latency_ms: int | None,
    ) -> OutboundMessage:
        metadata = dict(self.route.metadata)
        if self.route.publish_lifecycle and latency_ms is not None:
            metadata["latency_ms"] = int(latency_ms)
        event = (
            StreamedResponseEvent()
            if self.route.publish_lifecycle
            and streamed
            and stop_reason not in {"error", "tool_error"}
            else None
        )
        return OutboundMessage(
            channel=self.route.channel,
            chat_id=self.route.chat_id,
            content=content or "Background task completed.",
            metadata=metadata,
            event=event,
        )

    async def complete(
        self,
        response: OutboundMessage | None,
        *,
        publish_completion: bool,
    ) -> None:
        completed_channel = self.lifecycle_message.channel
        completed_chat_id = self.lifecycle_message.chat_id
        if response is not None:
            await self.bus.publish_outbound(response)
            completed_channel = response.channel
            completed_chat_id = response.chat_id
        elif self.lifecycle_message.channel == "cli":
            await self.bus.publish_outbound(
                OutboundMessage(
                    channel=self.lifecycle_message.channel,
                    chat_id=self.lifecycle_message.chat_id,
                    content="",
                    metadata=dict(self.lifecycle_message.metadata or {}),
                )
            )
        if publish_completion:
            await self.runtime_event_publisher.turn_completed(
                channel=completed_channel,
                chat_id=completed_chat_id,
                session_key=self.session_key,
                metadata=self.lifecycle_message.metadata,
            )

    async def fail(self, *, publish_completion: bool) -> None:
        await self.bus.publish_outbound(
            OutboundMessage(
                channel=self.lifecycle_message.channel,
                chat_id=self.lifecycle_message.chat_id,
                content="Sorry, I encountered an error.",
                metadata=dict(self.lifecycle_message.metadata or {}),
            )
        )
        if publish_completion:
            await self.runtime_event_publisher.turn_completed(
                channel=self.lifecycle_message.channel,
                chat_id=self.lifecycle_message.chat_id,
                session_key=self.session_key,
                metadata=self.lifecycle_message.metadata,
            )

    async def idle(self) -> None:
        await self.runtime_event_publisher.run_status_changed(
            self.lifecycle_message,
            self.session_key,
            "idle",
        )
        self.runtime_event_publisher.clear_turn(self.session_key)

    def _stream_id(self) -> str:
        assert self._stream_base_id is not None
        return f"{self._stream_base_id}:{self._stream_segment}"

    async def _publish_stream(self, delta: str) -> None:
        await self.bus.publish_outbound(
            outbound_message_for_event(
                channel=self.delivery_message.channel,
                chat_id=self.delivery_message.chat_id,
                event=StreamDeltaEvent(content=delta, stream_id=self._stream_id()),
                metadata=self.delivery_message.metadata,
            )
        )
        self._stream_open = True

    async def _publish_stream_end(
        self,
        *,
        resuming: bool = False,
        merge_next: bool = False,
    ) -> None:
        await self.bus.publish_outbound(
            outbound_message_for_event(
                channel=self.delivery_message.channel,
                chat_id=self.delivery_message.chat_id,
                event=StreamEndEvent(
                    stream_id=self._stream_id(),
                    resuming=resuming,
                    merge_next=merge_next,
                ),
                metadata=self.delivery_message.metadata,
            )
        )
        self._stream_open = merge_next
        if not merge_next:
            self._stream_segment += 1

    async def abort_stream(self) -> None:
        """Close an interrupted stream so stateful channels can release its buffer."""
        if self._stream_open:
            await self._publish_stream_end()
